From d4eadef3740f0067851e3f25df67501d4909769c Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:45:01 -0700 Subject: [PATCH] Add interactive REST API docs with full endpoint tester and complete metadata serialization --- api/library.py | 10 +- api/serializers.py | 60 +++ database/music_database.py | 11 +- webui/static/docs.js | 1023 ++++++++++++++++++++++++------------ webui/static/style.css | 377 +++++++++++++ 5 files changed, 1152 insertions(+), 329 deletions(-) diff --git a/api/library.py b/api/library.py index 2562d1bf..b5f363c5 100644 --- a/api/library.py +++ b/api/library.py @@ -260,7 +260,7 @@ def register_routes(bp): Query params: type: 'artist', 'album', or 'track' (required) - provider: 'spotify', 'musicbrainz', 'itunes', 'deezer', 'audiodb' (required) + provider: 'spotify', 'musicbrainz', 'itunes', 'deezer', 'audiodb', 'tidal', 'qobuz', 'genius' (required) id: the external ID value (required) """ entity_type = request.args.get("type") @@ -276,8 +276,12 @@ def register_routes(bp): if not table: return api_error("BAD_REQUEST", "type must be 'artist', 'album', or 'track'.", 400) - if provider not in ("spotify", "musicbrainz", "itunes", "deezer", "audiodb"): - return api_error("BAD_REQUEST", "provider must be spotify, musicbrainz, itunes, deezer, or audiodb.", 400) + # genius only exists on artists and tracks, not albums + valid_providers = ("spotify", "musicbrainz", "itunes", "deezer", "audiodb", "tidal", "qobuz", "genius") + if provider not in valid_providers: + return api_error("BAD_REQUEST", f"provider must be one of: {', '.join(valid_providers)}.", 400) + if provider == "genius" and entity_type == "album": + return api_error("BAD_REQUEST", "Genius IDs are not available for albums. Use artist or track.", 400) try: db = get_database() diff --git a/api/serializers.py b/api/serializers.py index de410c4b..7ec8c002 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -86,18 +86,40 @@ def serialize_artist(obj, fields: Optional[Set[str]] = None) -> dict: "itunes_artist_id": d.get("itunes_artist_id"), "audiodb_id": d.get("audiodb_id"), "deezer_id": d.get("deezer_id"), + "tidal_id": d.get("tidal_id"), + "qobuz_id": d.get("qobuz_id"), + "genius_id": d.get("genius_id"), # Match statuses "musicbrainz_match_status": d.get("musicbrainz_match_status"), "spotify_match_status": d.get("spotify_match_status"), "itunes_match_status": d.get("itunes_match_status"), "audiodb_match_status": d.get("audiodb_match_status"), "deezer_match_status": d.get("deezer_match_status"), + "lastfm_match_status": d.get("lastfm_match_status"), + "genius_match_status": d.get("genius_match_status"), + "tidal_match_status": d.get("tidal_match_status"), + "qobuz_match_status": d.get("qobuz_match_status"), # Last attempted timestamps "musicbrainz_last_attempted": _isoformat(d.get("musicbrainz_last_attempted")), "spotify_last_attempted": _isoformat(d.get("spotify_last_attempted")), "itunes_last_attempted": _isoformat(d.get("itunes_last_attempted")), "audiodb_last_attempted": _isoformat(d.get("audiodb_last_attempted")), "deezer_last_attempted": _isoformat(d.get("deezer_last_attempted")), + "lastfm_last_attempted": _isoformat(d.get("lastfm_last_attempted")), + "genius_last_attempted": _isoformat(d.get("genius_last_attempted")), + "tidal_last_attempted": _isoformat(d.get("tidal_last_attempted")), + "qobuz_last_attempted": _isoformat(d.get("qobuz_last_attempted")), + # Last.fm metadata + "lastfm_listeners": d.get("lastfm_listeners"), + "lastfm_playcount": d.get("lastfm_playcount"), + "lastfm_tags": d.get("lastfm_tags"), + "lastfm_similar": d.get("lastfm_similar"), + "lastfm_bio": d.get("lastfm_bio"), + "lastfm_url": d.get("lastfm_url"), + # Genius metadata + "genius_description": d.get("genius_description"), + "genius_alt_names": d.get("genius_alt_names"), + "genius_url": d.get("genius_url"), } # Preserve extra keys from enriched queries (album_count, track_count, is_watched) for extra_key in ("album_count", "track_count", "is_watched", "image_url"): @@ -126,24 +148,40 @@ def serialize_album(obj, fields: Optional[Set[str]] = None) -> dict: "server_source": d.get("server_source"), "created_at": _isoformat(d.get("created_at")), "updated_at": _isoformat(d.get("updated_at")), + "upc": d.get("upc"), + "copyright": d.get("copyright"), # External IDs "musicbrainz_release_id": d.get("musicbrainz_release_id"), "spotify_album_id": d.get("spotify_album_id"), "itunes_album_id": d.get("itunes_album_id"), "audiodb_id": d.get("audiodb_id"), "deezer_id": d.get("deezer_id"), + "tidal_id": d.get("tidal_id"), + "qobuz_id": d.get("qobuz_id"), # Match statuses "musicbrainz_match_status": d.get("musicbrainz_match_status"), "spotify_match_status": d.get("spotify_match_status"), "itunes_match_status": d.get("itunes_match_status"), "audiodb_match_status": d.get("audiodb_match_status"), "deezer_match_status": d.get("deezer_match_status"), + "lastfm_match_status": d.get("lastfm_match_status"), + "tidal_match_status": d.get("tidal_match_status"), + "qobuz_match_status": d.get("qobuz_match_status"), # Last attempted timestamps "musicbrainz_last_attempted": _isoformat(d.get("musicbrainz_last_attempted")), "spotify_last_attempted": _isoformat(d.get("spotify_last_attempted")), "itunes_last_attempted": _isoformat(d.get("itunes_last_attempted")), "audiodb_last_attempted": _isoformat(d.get("audiodb_last_attempted")), "deezer_last_attempted": _isoformat(d.get("deezer_last_attempted")), + "lastfm_last_attempted": _isoformat(d.get("lastfm_last_attempted")), + "tidal_last_attempted": _isoformat(d.get("tidal_last_attempted")), + "qobuz_last_attempted": _isoformat(d.get("qobuz_last_attempted")), + # Last.fm metadata + "lastfm_listeners": d.get("lastfm_listeners"), + "lastfm_playcount": d.get("lastfm_playcount"), + "lastfm_tags": d.get("lastfm_tags"), + "lastfm_wiki": d.get("lastfm_wiki"), + "lastfm_url": d.get("lastfm_url"), } return filter_fields(result, fields) @@ -169,24 +207,46 @@ def serialize_track(obj, fields: Optional[Set[str]] = None) -> dict: "server_source": d.get("server_source"), "created_at": _isoformat(d.get("created_at")), "updated_at": _isoformat(d.get("updated_at")), + "isrc": d.get("isrc"), + "copyright": d.get("copyright"), # External IDs "musicbrainz_recording_id": d.get("musicbrainz_recording_id"), "spotify_track_id": d.get("spotify_track_id"), "itunes_track_id": d.get("itunes_track_id"), "audiodb_id": d.get("audiodb_id"), "deezer_id": d.get("deezer_id"), + "tidal_id": d.get("tidal_id"), + "qobuz_id": d.get("qobuz_id"), + "genius_id": d.get("genius_id"), # Match statuses "musicbrainz_match_status": d.get("musicbrainz_match_status"), "spotify_match_status": d.get("spotify_match_status"), "itunes_match_status": d.get("itunes_match_status"), "audiodb_match_status": d.get("audiodb_match_status"), "deezer_match_status": d.get("deezer_match_status"), + "lastfm_match_status": d.get("lastfm_match_status"), + "genius_match_status": d.get("genius_match_status"), + "tidal_match_status": d.get("tidal_match_status"), + "qobuz_match_status": d.get("qobuz_match_status"), # Last attempted timestamps "musicbrainz_last_attempted": _isoformat(d.get("musicbrainz_last_attempted")), "spotify_last_attempted": _isoformat(d.get("spotify_last_attempted")), "itunes_last_attempted": _isoformat(d.get("itunes_last_attempted")), "audiodb_last_attempted": _isoformat(d.get("audiodb_last_attempted")), "deezer_last_attempted": _isoformat(d.get("deezer_last_attempted")), + "lastfm_last_attempted": _isoformat(d.get("lastfm_last_attempted")), + "genius_last_attempted": _isoformat(d.get("genius_last_attempted")), + "tidal_last_attempted": _isoformat(d.get("tidal_last_attempted")), + "qobuz_last_attempted": _isoformat(d.get("qobuz_last_attempted")), + # Last.fm metadata + "lastfm_listeners": d.get("lastfm_listeners"), + "lastfm_playcount": d.get("lastfm_playcount"), + "lastfm_tags": d.get("lastfm_tags"), + "lastfm_url": d.get("lastfm_url"), + # Genius metadata + "genius_lyrics": d.get("genius_lyrics"), + "genius_description": d.get("genius_description"), + "genius_url": d.get("genius_url"), } # Preserve extra keys from joined queries (artist_name, album_title) for extra_key in ("artist_name", "album_title"): diff --git a/database/music_database.py b/database/music_database.py index 31b752df..ec0e6e28 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -7141,7 +7141,8 @@ class MusicDatabase: Args: table: 'artists', 'albums', or 'tracks' - provider: 'spotify', 'musicbrainz', 'itunes', 'deezer', 'audiodb' + provider: 'spotify', 'musicbrainz', 'itunes', 'deezer', 'audiodb', + 'tidal', 'qobuz', 'genius' (genius: artists/tracks only) """ column_map = { "artists": { @@ -7150,6 +7151,9 @@ class MusicDatabase: "itunes": "itunes_artist_id", "deezer": "deezer_id", "audiodb": "audiodb_id", + "tidal": "tidal_id", + "qobuz": "qobuz_id", + "genius": "genius_id", }, "albums": { "spotify": "spotify_album_id", @@ -7157,6 +7161,8 @@ class MusicDatabase: "itunes": "itunes_album_id", "deezer": "deezer_id", "audiodb": "audiodb_id", + "tidal": "tidal_id", + "qobuz": "qobuz_id", }, "tracks": { "spotify": "spotify_track_id", @@ -7164,6 +7170,9 @@ class MusicDatabase: "itunes": "itunes_track_id", "deezer": "deezer_id", "audiodb": "audiodb_id", + "tidal": "tidal_id", + "qobuz": "qobuz_id", + "genius": "genius_id", }, } if table not in column_map or provider not in column_map[table]: diff --git a/webui/static/docs.js b/webui/static/docs.js index 0d95e9b3..61b2f361 100644 --- a/webui/static/docs.js +++ b/webui/static/docs.js @@ -1356,343 +1356,716 @@ const DOCS_SECTIONS = [ icon: '/static/settings.jpg', children: [ { id: 'api-auth', title: 'Authentication' }, - { id: 'api-system', title: 'System & Status' }, + { id: 'api-system', title: 'System' }, + { id: 'api-library', title: 'Library' }, { id: 'api-search', title: 'Search' }, { id: 'api-downloads', title: 'Downloads' }, - { id: 'api-library', title: 'Library' }, - { id: 'api-library-edit', title: 'Library Editing' }, { id: 'api-playlists', title: 'Playlists' }, - { id: 'api-watchlist', title: 'Watchlist & Wishlist' }, - { id: 'api-automations', title: 'Automations' }, - { id: 'api-import', title: 'Import' }, - { id: 'api-settings', title: 'Settings' }, - { id: 'api-enrichment', title: 'Enrichment Workers' }, + { id: 'api-watchlist', title: 'Watchlist' }, + { id: 'api-wishlist', title: 'Wishlist' }, + { id: 'api-discover', title: 'Discover' }, { id: 'api-profiles', title: 'Profiles' }, + { id: 'api-settings', title: 'Settings & Keys' }, + { id: 'api-retag', title: 'Retag' }, + { id: 'api-cache', title: 'Cache' }, + { id: 'api-listenbrainz', title: 'ListenBrainz' }, { id: 'api-websocket', title: 'WebSocket Events' } ], - content: () => ` -
Generate API keys in Settings → API Keys. Use them via header or query parameter:
-Authorization: Bearer sk_xxxxx?api_key=sk_xxxxxKeys use a sk_ prefix. The raw key is shown once at creation; only a SHA-256 hash is stored.
Endpoints for checking system health, service connectivity, and library statistics.
-| Method | Endpoint | Description |
|---|---|---|
| GET | /api/system/status | Uptime, version, and service connectivity |
| GET | /api/system/stats | Library counts (artists, albums, tracks) and total size |
| GET | /api/system/activity | Recent activity feed entries |
| GET | /api/debug-info | Full debug snapshot: services, paths, workers, recent logs |
| GET | /api/version | Current version and update availability |
Search for music across metadata sources and Soulseek.
-| Method | Endpoint | Description |
|---|---|---|
| POST | /api/enhanced-search | Search Spotify/iTunes for artists, albums, and tracks |
| POST | /api/search | Search Soulseek directly for files |
| POST | /api/spotify/search_tracks | Search Spotify for tracks by query |
| POST | /api/itunes/search_tracks | Search iTunes/Apple Music for tracks |
Start, monitor, and manage music downloads.
-| Method | Endpoint | Description |
|---|---|---|
| POST | /api/download | Start downloading a track |
| GET | /api/downloads/status | Current download queue and progress |
| POST | /api/download/cancel | Cancel an active download |
| POST | /api/download/album | Start downloading an entire album |
| GET | /api/downloads/history | Completed and failed download history |
?fields= for field selection and pagination via ?page= and ?limit=.',
+ endpoints: [
+ E('GET', '/library/artists', 'List library artists with search, letter filter, and pagination', [
+ P('search', 'string', false, 'Substring filter on artist name', '""'),
+ P('letter', 'string', false, 'Filter by first letter, or "all"', '"all"'),
+ P('watchlist', 'string', false, 'Filter by watchlist status', '"all"'),
+ P('page', 'int', false, 'Page number', '1'),
+ P('limit', 'int', false, 'Results per page (max 200)', '50'),
+ P('fields', 'string', false, 'Comma-separated field names to include', 'all')
+ ], null, {
+ response: '{\n "success": true,\n "data": {\n "artists": [\n {\n "id": 1,\n "name": "Radiohead",\n "thumb_url": "https://...",\n "banner_url": "https://...",\n "genres": ["alternative rock", "art rock"],\n "summary": "English rock band...",\n "style": "Alternative/Indie", "mood": "Melancholy",\n "label": "XL Recordings",\n "musicbrainz_id": "a74b1b7f-...",\n "spotify_artist_id": "4Z8W4fKeB5YxbusRsdQVPb",\n "itunes_artist_id": "657515",\n "deezer_id": "399", "tidal_id": "3746724",\n "qobuz_id": "61592", "genius_id": "604",\n "lastfm_listeners": 5832451,\n "lastfm_playcount": 328456789,\n "genius_url": "https://genius.com/artists/Radiohead",\n "album_count": 9, "track_count": 101,\n "...": "all 50+ fields included"\n }\n ]\n },\n "pagination": {\n "page": 1, "limit": 50, "total": 342, "total_pages": 7,\n "has_next": true, "has_prev": false\n }\n}'
+ }),
+ E('GET', '/library/artists/{artist_id}', 'Get a single artist with all metadata and album list', [
+ P('fields', 'string', false, 'Comma-separated fields', 'all')
+ ], null, {
+ response: '{\n "success": true,\n "data": {\n "artist": {\n "id": 1, "name": "Radiohead",\n "thumb_url": "https://...", "banner_url": "https://...",\n "genres": ["alternative rock", "art rock"],\n "summary": "English rock band formed in 1985...",\n "style": "Alternative/Indie", "mood": "Melancholy",\n "label": "XL Recordings",\n "server_source": "plex",\n "created_at": "2026-01-15T10:00:00Z",\n "updated_at": "2026-03-13T08:00:00Z",\n "musicbrainz_id": "a74b1b7f-71a3-4b73-8c51-5c1f3a71c9e8",\n "spotify_artist_id": "4Z8W4fKeB5YxbusRsdQVPb",\n "itunes_artist_id": "657515",\n "audiodb_id": "111239",\n "deezer_id": "399",\n "tidal_id": "3746724",\n "qobuz_id": "61592",\n "genius_id": "604",\n "musicbrainz_match_status": "matched",\n "spotify_match_status": "matched",\n "itunes_match_status": "matched",\n "audiodb_match_status": "matched",\n "deezer_match_status": "matched",\n "lastfm_match_status": "matched",\n "genius_match_status": "matched",\n "tidal_match_status": "matched",\n "qobuz_match_status": "matched",\n "musicbrainz_last_attempted": "2026-03-10T08:00:00Z",\n "spotify_last_attempted": "2026-03-10T08:00:00Z",\n "itunes_last_attempted": "2026-03-10T08:00:00Z",\n "audiodb_last_attempted": "2026-03-10T08:00:00Z",\n "deezer_last_attempted": "2026-03-10T08:00:00Z",\n "lastfm_last_attempted": "2026-03-10T08:00:00Z",\n "genius_last_attempted": "2026-03-10T08:00:00Z",\n "tidal_last_attempted": "2026-03-10T08:00:00Z",\n "qobuz_last_attempted": "2026-03-10T08:00:00Z",\n "lastfm_listeners": 5832451,\n "lastfm_playcount": 328456789,\n "lastfm_tags": "alternative, rock, experimental",\n "lastfm_similar": "Thom Yorke, Atoms for Peace, Portishead",\n "lastfm_bio": "Radiohead are an English rock band...",\n "lastfm_url": "https://www.last.fm/music/Radiohead",\n "genius_description": "Radiohead is an English rock band...",\n "genius_alt_names": "On a Friday",\n "genius_url": "https://genius.com/artists/Radiohead",\n "album_count": 9, "track_count": 101\n },\n "albums": [\n { "id": 10, "title": "OK Computer", "year": 1997, "track_count": 12, "record_type": "album" }\n ]\n }\n}'
+ }),
+ E('GET', '/library/artists/{artist_id}/albums', 'List albums for an artist', [
+ P('fields', 'string', false, 'Comma-separated fields', 'all')
+ ], null, {
+ response: '{\n "success": true,\n "data": {\n "albums": [\n {\n "id": 10, "artist_id": 1, "title": "OK Computer", "year": 1997,\n "thumb_url": "https://...", "track_count": 12, "duration": 3214000,\n "genres": ["alternative rock"],\n "style": "Art Rock", "mood": "Atmospheric",\n "label": "Parlophone", "record_type": "album", "explicit": false,\n "upc": "0724385522529", "copyright": "1997 Parlophone Records",\n "spotify_album_id": "6dVIqQ8qmQ5GBnJ9shOYGE",\n "tidal_id": "17914997", "qobuz_id": "0724385522529",\n "lastfm_listeners": 1543000, "lastfm_playcount": 89234567,\n "...": "all 45+ fields included"\n }\n ]\n }\n}'
+ }),
+ E('GET', '/library/albums', 'List or search albums with pagination', [
+ P('search', 'string', false, 'Substring filter on album title', '""'),
+ P('artist_id', 'int', false, 'Filter by artist ID'),
+ P('year', 'int', false, 'Filter by release year'),
+ P('page', 'int', false, 'Page number', '1'),
+ P('limit', 'int', false, 'Results per page (max 200)', '50'),
+ P('fields', 'string', false, 'Comma-separated fields', 'all')
+ ], null, {
+ response: '{\n "success": true,\n "data": { "albums": [ { "id": 10, "title": "OK Computer", "year": 1997, "artist_id": 1 } ] },\n "pagination": { "page": 1, "limit": 50, "total": 1205, "total_pages": 25, "has_next": true, "has_prev": false }\n}'
+ }),
+ E('GET', '/library/albums/{album_id}', 'Get a single album with metadata and embedded tracks', [
+ P('fields', 'string', false, 'Comma-separated fields', 'all')
+ ], null, {
+ response: '{\n "success": true,\n "data": {\n "album": {\n "id": 10, "artist_id": 1, "title": "OK Computer", "year": 1997,\n "thumb_url": "https://...",\n "genres": ["alternative rock"],\n "track_count": 12, "duration": 3214000,\n "style": "Art Rock", "mood": "Atmospheric",\n "label": "Parlophone", "explicit": false, "record_type": "album",\n "server_source": "plex",\n "created_at": "2026-01-15T10:00:00Z",\n "updated_at": "2026-03-13T08:00:00Z",\n "upc": "0724385522529", "copyright": "1997 Parlophone Records",\n "musicbrainz_release_id": "b1a9c0e7-...",\n "spotify_album_id": "6dVIqQ8qmQ5GBnJ9shOYGE",\n "itunes_album_id": "1097861387",\n "audiodb_id": "2115888",\n "deezer_id": "6575789",\n "tidal_id": "17914997",\n "qobuz_id": "0724385522529",\n "musicbrainz_match_status": "matched",\n "spotify_match_status": "matched",\n "itunes_match_status": "matched",\n "audiodb_match_status": "matched",\n "deezer_match_status": "matched",\n "lastfm_match_status": "matched",\n "tidal_match_status": "matched",\n "qobuz_match_status": "matched",\n "musicbrainz_last_attempted": "2026-03-10T08:00:00Z",\n "spotify_last_attempted": "2026-03-10T08:00:00Z",\n "itunes_last_attempted": "2026-03-10T08:00:00Z",\n "audiodb_last_attempted": "2026-03-10T08:00:00Z",\n "deezer_last_attempted": "2026-03-10T08:00:00Z",\n "lastfm_last_attempted": "2026-03-10T08:00:00Z",\n "tidal_last_attempted": "2026-03-10T08:00:00Z",\n "qobuz_last_attempted": "2026-03-10T08:00:00Z",\n "lastfm_listeners": 1543000,\n "lastfm_playcount": 89234567,\n "lastfm_tags": "alternative, 90s, rock",\n "lastfm_wiki": "OK Computer is the third studio album...",\n "lastfm_url": "https://www.last.fm/music/Radiohead/OK+Computer"\n },\n "tracks": [\n { "id": 100, "title": "Airbag", "track_number": 1, "duration": 284000, "bitrate": 1411 }\n ]\n }\n}'
+ }),
+ E('GET', '/library/albums/{album_id}/tracks', 'List tracks in an album', [
+ P('fields', 'string', false, 'Comma-separated fields', 'all')
+ ], null, {
+ response: '{\n "success": true,\n "data": {\n "tracks": [\n {\n "id": 100, "album_id": 10, "artist_id": 1, "title": "Airbag",\n "track_number": 1, "duration": 284000,\n "file_path": "/music/Radiohead/OK Computer/01 Airbag.flac",\n "bitrate": 1411, "bpm": 120.5, "explicit": false,\n "isrc": "GBAYE9700106",\n "spotify_track_id": "6anwyDGQmsg45JKiVKpKGA",\n "tidal_id": "17914998", "genius_id": "1342",\n "lastfm_listeners": 892000, "lastfm_playcount": 4567890,\n "genius_url": "https://genius.com/Radiohead-airbag-lyrics",\n "...": "all 55+ fields included"\n }\n ]\n }\n}'
+ }),
+ E('GET', '/library/tracks/{track_id}', 'Get a single track with all metadata', [
+ P('fields', 'string', false, 'Comma-separated fields', 'all')
+ ], null, {
+ response: '{\n "success": true,\n "data": {\n "track": {\n "id": 100, "album_id": 10, "artist_id": 1, "title": "Airbag",\n "track_number": 1, "duration": 284000,\n "file_path": "/music/Radiohead/OK Computer/01 Airbag.flac",\n "bitrate": 1411, "bpm": 120.5, "explicit": false,\n "style": "Art Rock", "mood": "Atmospheric",\n "repair_status": null, "repair_last_checked": null,\n "server_source": "plex",\n "created_at": "2026-01-15T10:00:00Z",\n "updated_at": "2026-03-13T08:00:00Z",\n "isrc": "GBAYE9700106", "copyright": "1997 Parlophone Records",\n "musicbrainz_recording_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",\n "spotify_track_id": "6anwyDGQmsg45JKiVKpKGA",\n "itunes_track_id": "1097861700",\n "audiodb_id": null,\n "deezer_id": "72420132",\n "tidal_id": "17914998",\n "qobuz_id": "24517824",\n "genius_id": "1342",\n "musicbrainz_match_status": "matched",\n "spotify_match_status": "matched",\n "itunes_match_status": "matched",\n "audiodb_match_status": "not_found",\n "deezer_match_status": "matched",\n "lastfm_match_status": "matched",\n "genius_match_status": "matched",\n "tidal_match_status": "matched",\n "qobuz_match_status": "matched",\n "musicbrainz_last_attempted": "2026-03-10T08:00:00Z",\n "spotify_last_attempted": "2026-03-10T08:00:00Z",\n "itunes_last_attempted": "2026-03-10T08:00:00Z",\n "audiodb_last_attempted": "2026-03-10T08:00:00Z",\n "deezer_last_attempted": "2026-03-10T08:00:00Z",\n "lastfm_last_attempted": "2026-03-10T08:00:00Z",\n "genius_last_attempted": "2026-03-10T08:00:00Z",\n "tidal_last_attempted": "2026-03-10T08:00:00Z",\n "qobuz_last_attempted": "2026-03-10T08:00:00Z",\n "lastfm_listeners": 892000,\n "lastfm_playcount": 4567890,\n "lastfm_tags": "alternative rock, radiohead",\n "lastfm_url": "https://www.last.fm/music/Radiohead/_/Airbag",\n "genius_lyrics": "In the next world war, in a jackknifed juggernaut...",\n "genius_description": "The opening track of OK Computer...",\n "genius_url": "https://genius.com/Radiohead-airbag-lyrics"\n }\n }\n}'
+ }),
+ E('GET', '/library/tracks', 'Search tracks by title and/or artist', [
+ P('title', 'string', false, 'Track title to search (at least one of title/artist required)', '""'),
+ P('artist', 'string', false, 'Artist name to search', '""'),
+ P('limit', 'int', false, 'Max results (max 200)', '50'),
+ P('fields', 'string', false, 'Comma-separated fields', 'all')
+ ], null, {
+ response: '{\n "success": true,\n "data": {\n "tracks": [\n { "id": 100, "title": "Airbag", "artist_name": "Radiohead", "album_title": "OK Computer" }\n ]\n }\n}'
+ }),
+ E('GET', '/library/genres', 'List all genres with occurrence counts', [
+ P('source', 'string', false, '"artists" or "albums"', '"artists"')
+ ], null, {
+ response: '{\n "success": true,\n "data": {\n "genres": [ { "name": "alternative rock", "count": 45 }, { "name": "electronic", "count": 38 } ],\n "source": "artists"\n }\n}'
+ }),
+ E('GET', '/library/recently-added', 'Get recently added content', [
+ P('type', 'string', false, '"albums", "artists", or "tracks"', '"albums"'),
+ P('limit', 'int', false, 'Max items (max 200)', '50'),
+ P('fields', 'string', false, 'Comma-separated fields', 'all')
+ ], null, {
+ response: '{\n "success": true,\n "data": {\n "items": [ { "id": 10, "title": "OK Computer", "year": 1997, "created_at": "2026-03-12T10:00:00Z" } ],\n "type": "albums"\n }\n}'
+ }),
+ E('GET', '/library/lookup', 'Look up a library entity by external provider ID', [
+ P('type', 'string', true, '"artist", "album", or "track"'),
+ P('provider', 'string', true, '"spotify", "musicbrainz", "itunes", "deezer", "audiodb", "tidal", "qobuz", or "genius"'),
+ P('id', 'string', true, 'The external ID value'),
+ P('fields', 'string', false, 'Comma-separated fields', 'all')
+ ], null, {
+ response: '{\n "success": true,\n "data": {\n "artist": { "id": 1, "name": "Radiohead", "spotify_artist_id": "4Z8W4fKeB5YxbusRsdQVPb" }\n }\n}'
+ }),
+ E('GET', '/library/stats', 'Get library statistics', [], null, {
+ response: '{\n "success": true,\n "data": {\n "artists": 342,\n "albums": 1205,\n "tracks": 14832,\n "database_size_mb": 45.2,\n "last_update": "2026-03-13T08:00:00Z"\n }\n}'
+ })
+ ]
+ },
+ {
+ id: 'api-search', title: 'Search', desc: 'Search external music sources (Spotify, iTunes, Hydrabase). All search endpoints use POST with a JSON body.',
+ endpoints: [
+ E('POST', '/search/tracks', 'Search for tracks across music sources', [], [
+ P('query', 'string', true, 'Search query'),
+ P('source', 'string', false, '"spotify", "itunes", or "auto"', '"auto"'),
+ P('limit', 'int', false, 'Max results (1-50)', '20')
+ ], {
+ request: '{\n "query": "Karma Police",\n "source": "auto",\n "limit": 10\n}',
+ response: '{\n "success": true,\n "data": {\n "tracks": [\n {\n "id": "3SVAN3BRByDmHOhKyIDxfC",\n "name": "Karma Police",\n "artists": ["Radiohead"],\n "album": "OK Computer",\n "duration_ms": 264066,\n "popularity": 78,\n "image_url": "https://...",\n "release_date": "1997-05-28"\n }\n ],\n "source": "spotify"\n }\n}'
+ }),
+ E('POST', '/search/albums', 'Search for albums', [], [
+ P('query', 'string', true, 'Search query'),
+ P('limit', 'int', false, 'Max results (1-50)', '20')
+ ], {
+ request: '{\n "query": "OK Computer",\n "limit": 5\n}',
+ response: '{\n "success": true,\n "data": {\n "albums": [\n {\n "id": "6dVIqQ8qmQ5GBnJ9shOYGE",\n "name": "OK Computer",\n "artists": ["Radiohead"],\n "release_date": "1997-05-28",\n "total_tracks": 12,\n "album_type": "album",\n "image_url": "https://..."\n }\n ],\n "source": "spotify"\n }\n}'
+ }),
+ E('POST', '/search/artists', 'Search for artists', [], [
+ P('query', 'string', true, 'Search query'),
+ P('limit', 'int', false, 'Max results (1-50)', '20')
+ ], {
+ request: '{\n "query": "Radiohead",\n "limit": 5\n}',
+ response: '{\n "success": true,\n "data": {\n "artists": [\n {\n "id": "4Z8W4fKeB5YxbusRsdQVPb",\n "name": "Radiohead",\n "popularity": 79,\n "genres": ["alternative rock", "art rock"],\n "followers": 8500000,\n "image_url": "https://..."\n }\n ],\n "source": "spotify"\n }\n}'
+ })
+ ]
+ },
+ {
+ id: 'api-downloads', title: 'Downloads', desc: 'List active downloads, cancel individual or all downloads.',
+ endpoints: [
+ E('GET', '/downloads', 'List active and recent download tasks', [], null, {
+ response: '{\n "success": true,\n "data": {\n "downloads": [\n {\n "id": "abc123",\n "status": "downloading",\n "track_name": "Karma Police",\n "artist_name": "Radiohead",\n "album_name": "OK Computer",\n "username": "slsk_user42",\n "progress": 67,\n "size": 34500000,\n "batch_id": null,\n "error": null\n }\n ]\n }\n}'
+ }),
+ E('POST', '/downloads/{download_id}/cancel', 'Cancel a specific download', [], [
+ P('username', 'string', true, 'Soulseek username for the transfer')
+ ], {
+ request: '{\n "username": "slsk_user42"\n}',
+ response: '{\n "success": true,\n "data": { "message": "Download cancelled." }\n}'
+ }),
+ E('POST', '/downloads/cancel-all', 'Cancel all active downloads and clear completed', [], null, {
+ response: '{\n "success": true,\n "data": { "message": "All downloads cancelled and cleared." }\n}'
+ })
+ ]
+ },
+ {
+ id: 'api-playlists', title: 'Playlists', desc: 'List and inspect playlists from Spotify or Tidal, and trigger playlist sync.',
+ endpoints: [
+ E('GET', '/playlists', 'List user playlists from Spotify or Tidal', [
+ P('source', 'string', false, '"spotify" or "tidal"', '"spotify"')
+ ], null, {
+ response: '{\n "success": true,\n "data": {\n "playlists": [\n {\n "id": "37i9dQZF1DXcBWIGoYBM5M",\n "name": "Today\'s Top Hits",\n "owner": "spotify",\n "track_count": 50,\n "image_url": "https://..."\n }\n ],\n "source": "spotify"\n }\n}'
+ }),
+ E('GET', '/playlists/{playlist_id}', 'Get playlist details with tracks', [
+ P('source', 'string', false, 'Only "spotify" is supported', '"spotify"')
+ ], null, {
+ response: '{\n "success": true,\n "data": {\n "playlist": {\n "id": "37i9dQZF1DXcBWIGoYBM5M",\n "name": "Today\'s Top Hits",\n "owner": "spotify",\n "total_tracks": 50,\n "tracks": [\n {\n "id": "3SVAN3BRByDmHOhKyIDxfC",\n "name": "Karma Police",\n "artists": ["Radiohead"],\n "album": "OK Computer",\n "duration_ms": 264066,\n "image_url": "https://..."\n }\n ]\n },\n "source": "spotify"\n }\n}'
+ }),
+ E('POST', '/playlists/{playlist_id}/sync', 'Trigger playlist sync and download', [], [
+ P('playlist_name', 'string', true, 'Name of the playlist'),
+ P('tracks', 'array', true, 'Array of track objects to sync')
+ ], {
+ request: '{\n "playlist_name": "My Playlist",\n "tracks": [\n { "id": "3SVAN3...", "name": "Karma Police", "artists": [{ "name": "Radiohead" }] }\n ]\n}',
+ response: '{\n "success": true,\n "data": { "message": "Playlist sync started.", "playlist_id": "37i9dQZF1DXcBWIGoYBM5M" }\n}'
+ })
+ ]
+ },
+ {
+ id: 'api-watchlist', title: 'Watchlist', desc: 'View, add, remove watched artists and trigger new release scans. Profile-scoped via X-Profile-Id header.',
+ endpoints: [
+ E('GET', '/watchlist', 'List all watchlist artists for the current profile', [
+ P('fields', 'string', false, 'Comma-separated fields', 'all')
+ ], null, {
+ response: '{\n "success": true,\n "data": {\n "artists": [\n {\n "id": 1,\n "artist_name": "Radiohead",\n "spotify_artist_id": "4Z8W4fKeB5YxbusRsdQVPb",\n "image_url": "https://...",\n "date_added": "2026-01-15T10:00:00Z",\n "include_albums": true,\n "include_eps": true,\n "include_singles": true,\n "include_live": false,\n "include_remixes": false,\n "profile_id": 1\n }\n ]\n }\n}'
+ }),
+ E('POST', '/watchlist', 'Add an artist to the watchlist', [], [
+ P('artist_id', 'string', true, 'Spotify or iTunes artist ID'),
+ P('artist_name', 'string', true, 'Artist display name')
+ ], {
+ request: '{\n "artist_id": "4Z8W4fKeB5YxbusRsdQVPb",\n "artist_name": "Radiohead"\n}',
+ response: '{\n "success": true,\n "data": { "message": "Added Radiohead to watchlist." }\n}'
+ }),
+ E('DELETE', '/watchlist/{artist_id}', 'Remove an artist from the watchlist', [], null, {
+ response: '{\n "success": true,\n "data": { "message": "Artist removed from watchlist." }\n}'
+ }),
+ E('PATCH', '/watchlist/{artist_id}', 'Update content type filters for a watchlist artist', [], [
+ P('include_albums', 'bool', false, 'Include albums'),
+ P('include_eps', 'bool', false, 'Include EPs'),
+ P('include_singles', 'bool', false, 'Include singles'),
+ P('include_live', 'bool', false, 'Include live recordings'),
+ P('include_remixes', 'bool', false, 'Include remixes'),
+ P('include_acoustic', 'bool', false, 'Include acoustic versions'),
+ P('include_compilations', 'bool', false, 'Include compilations')
+ ], {
+ request: '{\n "include_live": true,\n "include_remixes": false\n}',
+ response: '{\n "success": true,\n "data": {\n "message": "Watchlist filters updated.",\n "updated": { "include_live": true, "include_remixes": false }\n }\n}'
+ }),
+ E('POST', '/watchlist/scan', 'Trigger a watchlist scan for new releases', [], null, {
+ response: '{\n "success": true,\n "data": { "message": "Watchlist scan started." }\n}'
+ })
+ ]
+ },
+ {
+ id: 'api-wishlist', title: 'Wishlist', desc: 'View, add, remove wishlist tracks and trigger download processing. Profile-scoped.',
+ endpoints: [
+ E('GET', '/wishlist', 'List wishlist tracks with optional category filter', [
+ P('category', 'string', false, '"singles" or "albums"', 'all'),
+ P('page', 'int', false, 'Page number', '1'),
+ P('limit', 'int', false, 'Results per page (max 200)', '50'),
+ P('fields', 'string', false, 'Comma-separated fields', 'all')
+ ], null, {
+ response: '{\n "success": true,\n "data": {\n "tracks": [\n {\n "id": 1,\n "spotify_track_id": "3SVAN3BRByDmHOhKyIDxfC",\n "track_name": "Karma Police",\n "artist_name": "Radiohead",\n "album_name": "OK Computer",\n "failure_reason": "No suitable source found",\n "retry_count": 2,\n "last_attempted": "2026-03-12T10:00:00Z",\n "date_added": "2026-03-10T08:00:00Z",\n "source_type": "playlist_sync"\n }\n ]\n },\n "pagination": { "page": 1, "limit": 50, "total": 12, "total_pages": 1, "has_next": false, "has_prev": false }\n}'
+ }),
+ E('POST', '/wishlist', 'Add a track to the wishlist', [], [
+ P('spotify_track_data', 'object', true, 'Full Spotify track data object'),
+ P('failure_reason', 'string', false, 'Reason for adding', '"Added via API"'),
+ P('source_type', 'string', false, 'Source identifier', '"api"')
+ ], {
+ request: '{\n "spotify_track_data": {\n "id": "3SVAN3BRByDmHOhKyIDxfC",\n "name": "Karma Police",\n "artists": [{ "name": "Radiohead" }],\n "album": { "name": "OK Computer", "album_type": "album" }\n },\n "source_type": "api"\n}',
+ response: '{\n "success": true,\n "data": { "message": "Track added to wishlist." }\n}'
+ }),
+ E('DELETE', '/wishlist/{track_id}', 'Remove a track by Spotify track ID', [], null, {
+ response: '{\n "success": true,\n "data": { "message": "Track removed from wishlist." }\n}'
+ }),
+ E('POST', '/wishlist/process', 'Trigger wishlist download processing', [], null, {
+ response: '{\n "success": true,\n "data": { "message": "Wishlist processing started." }\n}'
+ })
+ ]
+ },
+ {
+ id: 'api-discover', title: 'Discover', desc: 'Browse the discovery pool, similar artists, recent releases, and bubble snapshots. Profile-scoped.',
+ endpoints: [
+ E('GET', '/discover/pool', 'List discovery pool tracks with optional filters', [
+ P('new_releases_only', 'string', false, '"true" to filter new releases only', 'false'),
+ P('source', 'string', false, '"spotify" or "itunes"', 'all'),
+ P('page', 'int', false, 'Page number', '1'),
+ P('limit', 'int', false, 'Max tracks (max 500)', '100'),
+ P('fields', 'string', false, 'Comma-separated fields', 'all')
+ ], null, {
+ response: '{\n "success": true,\n "data": {\n "tracks": [\n {\n "id": 1,\n "spotify_track_id": "3SVAN3...",\n "track_name": "Karma Police",\n "artist_name": "Radiohead",\n "album_name": "OK Computer",\n "album_cover_url": "https://...",\n "duration_ms": 264066,\n "popularity": 78,\n "is_new_release": false,\n "source": "spotify"\n }\n ]\n },\n "pagination": { "page": 1, "limit": 100, "total": 850, "total_pages": 9, "has_next": true, "has_prev": false }\n}'
+ }),
+ E('GET', '/discover/similar-artists', 'List top similar artists from the watchlist', [
+ P('limit', 'int', false, 'Max artists (max 200)', '50'),
+ P('fields', 'string', false, 'Comma-separated fields', 'all')
+ ], null, {
+ response: '{\n "success": true,\n "data": {\n "artists": [\n {\n "id": 1,\n "similar_artist_name": "Thom Yorke",\n "similar_artist_spotify_id": "2x9SpqnPi8rlE9pjHBwN5z",\n "similarity_rank": 1,\n "occurrence_count": 5\n }\n ]\n }\n}'
+ }),
+ E('GET', '/discover/recent-releases', 'List recent releases from watched artists', [
+ P('limit', 'int', false, 'Max releases (max 200)', '50'),
+ P('fields', 'string', false, 'Comma-separated fields', 'all')
+ ], null, {
+ response: '{\n "success": true,\n "data": {\n "releases": [\n {\n "id": 1,\n "album_name": "A Moon Shaped Pool",\n "album_spotify_id": "2ix8vWvvSp2Yo7rKMiWpkg",\n "release_date": "2016-05-08",\n "album_cover_url": "https://...",\n "track_count": 11,\n "source": "spotify"\n }\n ]\n }\n}'
+ }),
+ E('GET', '/discover/pool/metadata', 'Get discovery pool metadata', [], null, {
+ response: '{\n "success": true,\n "data": {\n "last_populated": "2026-03-12T10:00:00Z",\n "track_count": 850,\n "updated_at": "2026-03-12T10:00:00Z"\n }\n}'
+ }),
+ E('GET', '/discover/bubbles', 'List all bubble snapshots for the current profile', [], null, {
+ response: '{\n "success": true,\n "data": {\n "snapshots": {\n "artist_bubbles": { "snapshot_data": [...], "updated_at": "..." },\n "search_bubbles": null,\n "discover_downloads": null\n }\n }\n}'
+ }),
+ E('GET', '/discover/bubbles/{snapshot_type}', 'Get a specific bubble snapshot (artist_bubbles, search_bubbles, discover_downloads)', [], null, {
+ response: '{\n "success": true,\n "data": {\n "snapshot": { "snapshot_data": [...], "updated_at": "2026-03-12T10:00:00Z" }\n }\n}'
+ })
+ ]
+ },
+ {
+ id: 'api-profiles', title: 'Profiles', desc: 'Manage multi-profile support. Create, update, delete profiles with PIN protection and page access control.',
+ endpoints: [
+ E('GET', '/profiles', 'List all profiles', [], null, {
+ response: '{\n "success": true,\n "data": {\n "profiles": [\n {\n "id": 1,\n "name": "Admin",\n "is_admin": 1,\n "avatar_color": "#6366f1",\n "avatar_url": null,\n "created_at": "2026-01-01T00:00:00Z"\n }\n ]\n }\n}'
+ }),
+ E('GET', '/profiles/{profile_id}', 'Get a single profile by ID', [], null, {
+ response: '{\n "success": true,\n "data": {\n "profile": {\n "id": 1, "name": "Admin", "is_admin": 1,\n "avatar_color": "#6366f1", "avatar_url": null\n }\n }\n}'
+ }),
+ E('POST', '/profiles', 'Create a new profile', [], [
+ P('name', 'string', true, 'Profile display name'),
+ P('avatar_color', 'string', false, 'Hex color for avatar', '"#6366f1"'),
+ P('avatar_url', 'string', false, 'Custom avatar image URL'),
+ P('is_admin', 'bool', false, 'Admin privileges', 'false'),
+ P('pin', 'string', false, 'PIN for profile protection')
+ ], {
+ request: '{\n "name": "Family Room",\n "is_admin": false,\n "avatar_color": "#22c55e",\n "pin": "1234"\n}',
+ response: '{\n "success": true,\n "data": {\n "profile": {\n "id": 3, "name": "Family Room", "is_admin": 0,\n "avatar_color": "#22c55e"\n }\n }\n}'
+ }),
+ E('PUT', '/profiles/{profile_id}', 'Update a profile', [], [
+ P('name', 'string', false, 'New display name'),
+ P('avatar_color', 'string', false, 'Hex color'),
+ P('avatar_url', 'string', false, 'Avatar image URL'),
+ P('is_admin', 'bool', false, 'Admin privileges'),
+ P('pin', 'string', false, 'New PIN (empty string clears PIN)')
+ ], {
+ request: '{\n "name": "Kids Room",\n "avatar_color": "#f59e0b"\n}',
+ response: '{\n "success": true,\n "data": {\n "profile": { "id": 3, "name": "Kids Room", "avatar_color": "#f59e0b" }\n }\n}'
+ }),
+ E('DELETE', '/profiles/{profile_id}', 'Delete a profile (cannot delete profile 1)', [], null, {
+ response: '{\n "success": true,\n "data": { "message": "Profile 3 deleted." }\n}'
+ })
+ ]
+ },
+ {
+ id: 'api-settings', title: 'Settings & API Keys', desc: 'Read and update application settings. Manage API keys. Sensitive values are always redacted in GET responses.',
+ endpoints: [
+ E('GET', '/settings', 'Get current settings (sensitive values redacted)', [], null, {
+ response: '{\n "success": true,\n "data": {\n "settings": {\n "spotify": {\n "client_id": "***REDACTED***",\n "country": "US"\n },\n "download_path": "/music",\n "download_source": "hybrid",\n "ui_appearance": {\n "accent_preset": "green",\n "particles_enabled": true\n }\n }\n }\n}'
+ }),
+ E('PATCH', '/settings', 'Update settings (partial, dot-notation keys accepted)', [], [
+ P('{key}', 'any', true, 'One or more key-value pairs. The "api_keys" key is blocked.')
+ ], {
+ request: '{\n "spotify.country": "GB",\n "download_path": "/new/music/path"\n}',
+ response: '{\n "success": true,\n "data": {\n "message": "Settings updated.",\n "updated_keys": ["spotify.country", "download_path"]\n }\n}'
+ }),
+ E('GET', '/api-keys', 'List all API keys (prefix and label only, never the full key)', [], null, {
+ response: '{\n "success": true,\n "data": {\n "keys": [\n {\n "id": "a1b2c3d4-...",\n "label": "My Bot",\n "key_prefix": "sk_AbCdEfGh",\n "created_at": "2026-03-01T10:00:00Z",\n "last_used_at": "2026-03-13T09:15:00Z"\n }\n ]\n }\n}'
+ }),
+ E('POST', '/api-keys', 'Generate a new API key (raw key returned once)', [], [
+ P('label', 'string', false, 'Descriptive label for the key', '""')
+ ], {
+ request: '{\n "label": "Home Assistant"\n}',
+ response: '{\n "success": true,\n "data": {\n "key": "sk_AbCdEfGhIjKlMnOpQrStUvWxYz123456789...",\n "id": "a1b2c3d4-...",\n "label": "Home Assistant",\n "key_prefix": "sk_AbCdEfGh",\n "created_at": "2026-03-13T10:00:00Z"\n }\n}'
+ }),
+ E('DELETE', '/api-keys/{key_id}', 'Revoke an API key by its UUID', [], null, {
+ response: '{\n "success": true,\n "data": { "message": "API key revoked." }\n}'
+ }),
+ E('POST', '/api-keys/bootstrap', 'Generate the first API key when none exist (NO AUTH REQUIRED)', [], [
+ P('label', 'string', false, 'Label for the key', '"Default"')
+ ], {
+ request: '{\n "label": "My First Key"\n}',
+ response: '{\n "success": true,\n "data": {\n "key": "sk_...",\n "id": "...",\n "label": "My First Key",\n "key_prefix": "sk_...",\n "created_at": "2026-03-13T10:00:00Z"\n }\n}'
+ })
+ ]
+ },
+ {
+ id: 'api-retag', title: 'Retag', desc: 'View and manage the pending metadata correction queue.',
+ endpoints: [
+ E('GET', '/retag/groups', 'List all retag groups with track counts', [], null, {
+ response: '{\n "success": true,\n "data": {\n "groups": [\n {\n "id": 1,\n "original_artist": "Radiohed",\n "corrected_artist": "Radiohead",\n "track_count": 5,\n "created_at": "2026-03-12T10:00:00Z"\n }\n ]\n }\n}'
+ }),
+ E('GET', '/retag/groups/{group_id}', 'Get a retag group with its tracks', [], null, {
+ response: '{\n "success": true,\n "data": {\n "group": { "id": 1, "original_artist": "Radiohed", "corrected_artist": "Radiohead" },\n "tracks": [\n { "id": 100, "title": "Airbag", "file_path": "/music/..." }\n ]\n }\n}'
+ }),
+ E('DELETE', '/retag/groups/{group_id}', 'Delete a retag group and its tracks', [], null, {
+ response: '{\n "success": true,\n "data": { "message": "Retag group 1 deleted." }\n}'
+ }),
+ E('DELETE', '/retag/groups', 'Delete all retag groups and tracks', [], null, {
+ response: '{\n "success": true,\n "data": { "message": "Cleared 5 retag groups." }\n}'
+ }),
+ E('GET', '/retag/stats', 'Get retag queue statistics', [], null, {
+ response: '{\n "success": true,\n "data": {\n "total_groups": 5,\n "total_tracks": 23,\n "pending": 18,\n "completed": 5\n }\n}'
+ })
+ ]
+ },
+ {
+ id: 'api-cache', title: 'Cache', desc: 'Browse MusicBrainz and discovery match caches for debugging and inspection.',
+ endpoints: [
+ E('GET', '/cache/musicbrainz', 'List cached MusicBrainz lookups', [
+ P('entity_type', 'string', false, '"artist", "album", or "track"'),
+ P('search', 'string', false, 'Filter by entity name'),
+ P('page', 'int', false, 'Page number', '1'),
+ P('limit', 'int', false, 'Results per page (max 200)', '50')
+ ], null, {
+ response: '{\n "success": true,\n "data": {\n "entries": [\n {\n "entity_type": "artist",\n "entity_name": "Radiohead",\n "musicbrainz_id": "a74b1b7f-...",\n "last_updated": "2026-03-12T10:00:00Z",\n "metadata_json": { "type": "Group", "country": "GB" }\n }\n ]\n },\n "pagination": { "page": 1, "limit": 50, "total": 342, "total_pages": 7, "has_next": true, "has_prev": false }\n}'
+ }),
+ E('GET', '/cache/musicbrainz/stats', 'Get MusicBrainz cache statistics', [], null, {
+ response: '{\n "success": true,\n "data": {\n "total": 1024,\n "matched": 890,\n "unmatched": 134,\n "by_type": { "artist": 342, "album": 450, "track": 232 }\n }\n}'
+ }),
+ E('GET', '/cache/discovery-matches', 'List cached discovery provider matches', [
+ P('provider', 'string', false, '"spotify", "itunes", etc.'),
+ P('search', 'string', false, 'Filter by title or artist'),
+ P('page', 'int', false, 'Page number', '1'),
+ P('limit', 'int', false, 'Results per page (max 200)', '50')
+ ], null, {
+ response: '{\n "success": true,\n "data": {\n "entries": [\n {\n "provider": "spotify",\n "original_title": "Karma Police",\n "original_artist": "Radiohead",\n "matched_data_json": { "id": "3SVAN3...", "confidence": 0.95 },\n "use_count": 3,\n "last_used_at": "2026-03-12T10:00:00Z"\n }\n ]\n },\n "pagination": { "page": 1, "limit": 50, "total": 5000, "total_pages": 100, "has_next": true, "has_prev": false }\n}'
+ }),
+ E('GET', '/cache/discovery-matches/stats', 'Get discovery match cache statistics', [], null, {
+ response: '{\n "success": true,\n "data": {\n "total": 5000,\n "total_uses": 18500,\n "avg_confidence": 0.872,\n "by_provider": { "spotify": 3200, "itunes": 1800 }\n }\n}'
+ })
+ ]
+ },
+ {
+ id: 'api-listenbrainz', title: 'ListenBrainz', desc: 'Browse cached ListenBrainz playlists and their tracks.',
+ endpoints: [
+ E('GET', '/listenbrainz/playlists', 'List cached ListenBrainz playlists', [
+ P('type', 'string', false, 'Filter by playlist_type (e.g. "weekly-jams")'),
+ P('page', 'int', false, 'Page number', '1'),
+ P('limit', 'int', false, 'Results per page (max 200)', '50')
+ ], null, {
+ response: '{\n "success": true,\n "data": {\n "playlists": [\n {\n "id": 1,\n "playlist_mbid": "a1b2c3d4-...",\n "title": "Weekly Jams for user",\n "playlist_type": "weekly-jams",\n "track_count": 50,\n "created_at": "2026-03-10T00:00:00Z"\n }\n ]\n },\n "pagination": { "page": 1, "limit": 50, "total": 12, "total_pages": 1, "has_next": false, "has_prev": false }\n}'
+ }),
+ E('GET', '/listenbrainz/playlists/{playlist_id}', 'Get a ListenBrainz playlist with tracks (ID or MBID)', [], null, {
+ response: '{\n "success": true,\n "data": {\n "playlist": {\n "id": 1,\n "playlist_mbid": "a1b2c3d4-...",\n "title": "Weekly Jams for user",\n "playlist_type": "weekly-jams"\n },\n "tracks": [\n {\n "id": 1,\n "position": 0,\n "recording_mbid": "e1f2g3h4-...",\n "title": "Karma Police",\n "artist": "Radiohead"\n }\n ]\n }\n}'
+ })
+ ]
+ }
+ ];
-{
- "artist": "Radiohead",
- "album": "OK Computer",
- "title": "Karma Police",
- "spotify_id": "3SVAN3BRByDmHOhKyIDxfC"
-}Browse and query your music library.
-| Method | Endpoint | Description |
|---|---|---|
| GET | /api/library/artists | Paginated artist list with search and filters |
| GET | /api/artist-detail/{id} | Full artist info, discography, and match status |
| GET | /api/library/artist/{id}/enhanced | Enhanced view: full tracks, tags, file paths |
| GET | /api/library/album/{id}/tracks | Track list for an album |
| POST | /api/database/update | Trigger library database refresh |
Edit metadata and write tags to files via the Enhanced Library Manager.
-| Method | Endpoint | Description |
|---|---|---|
| PUT | /api/library/artist/{id} | Update artist fields (name, genres, label, etc.) |
| PUT | /api/library/album/{id} | Update album fields |
| PUT | /api/library/track/{id} | Update track fields (title, track_number, bpm) |
| POST | /api/library/tracks/batch | Batch update multiple tracks at once |
| GET | /api/library/track/{id}/tag-preview | Preview tag diff (current file tags vs database) |
| POST | /api/library/track/{id}/write-tags | Write database metadata to audio file tags |
| POST | /api/library/tracks/write-tags-batch | Batch write tags to multiple files |
Import, sync, and manage mirrored playlists from external sources.
-| Method | Endpoint | Description |
|---|---|---|
| GET | /api/playlists/spotify | List Spotify playlists |
| POST | /api/playlists/parse | Parse a YouTube/Tidal playlist URL |
| GET | /api/playlists/mirrored | List all mirrored playlists |
| POST | /api/playlists/{id}/sync | Sync playlist tracks to wishlist |
| POST | /api/playlists/{id}/discover | Discover official metadata for playlist tracks |
| Name | Type | Required | Description |
|---|---|---|---|
| ' + p.name + ' | ' + p.type + ' | ' + req + ' | ' + p.desc + def + ' |
Manage watched artists and the download wishlist queue.
-| Method | Endpoint | Description |
|---|---|---|
| GET | /api/watchlist | List all watched artists |
| POST | /api/watchlist/add | Add an artist to the watchlist |
| DELETE | /api/watchlist/{id} | Remove artist from watchlist |
| GET | /api/wishlist | List all wishlist items with status |
| POST | /api/wishlist/process | Trigger wishlist processing |
| Field | Type | Required | Description |
|---|---|---|---|
| ' + p.name + ' | ' + p.type + ' | ' + req + ' | ' + p.desc + def + ' |
Create, manage, and trigger automations.
-| Method | Endpoint | Description |
|---|---|---|
| GET | /api/automations | List all automations with status |
| POST | /api/automations | Create a new automation |
| PUT | /api/automations/{id} | Update an automation |
| POST | /api/automations/{id}/run | Run an automation immediately |
| DELETE | /api/automations/{id} | Delete a custom automation |
Manage the staging folder and import music files.
-| Method | Endpoint | Description |
|---|---|---|
| GET | /api/import/staging | List files and folders in the staging directory |
| POST | /api/import/match | Auto-match staged files to album tracks |
| POST | /api/import/confirm | Confirm and execute an import |
| POST | /api/import/search/albums | Search for album matches for staged files |
Read and update application settings. Admin-only endpoints.
-| Method | Endpoint | Description |
|---|---|---|
| GET | /api/settings | Get all current settings |
| POST | /api/settings | Update settings (partial update supported) |
| POST | /api/database/backup | Create a database backup |
| GET | /api/database/backups | List available backups |
| POST | /api/database/restore | Restore from a backup |
Monitor and control the background metadata enrichment workers.
-| Method | Endpoint | Description |
|---|---|---|
| GET | /api/enrichment/status | Status of all enrichment workers |
| POST | /api/enrichment/pause | Pause a specific worker |
| POST | /api/enrichment/resume | Resume a paused worker |
| POST | /api/enrichment/reset | Reset worker progress and re-scan all items |
Manage multi-profile support. Admin-only for create/edit/delete.
-| Method | Endpoint | Description |
|---|---|---|
| GET | /api/profiles | List all profiles |
| POST | /api/profiles | Create a new profile |
| PUT | /api/profiles/{id} | Update a profile (name, avatar, permissions) |
| DELETE | /api/profiles/{id} | Delete a profile (admin only, cannot delete profile 1) |
| POST | /api/profiles/switch | Switch active profile (PIN required if set) |
SoulSync uses Socket.IO for real-time communication. The frontend connects automatically and receives live updates without polling. Connect to the same host/port as the web UI.
-| Event | Description |
|---|---|
download_progress | Per-track download progress (speed, ETA, percentage) |
download_complete | Track finished downloading and post-processing |
batch_progress | Album/playlist batch download status |
worker_status | Enrichment worker status (Spotify, MusicBrainz, Deezer, Tidal, Qobuz, etc.) |
scan_progress | Library scan, quality scan, or duplicate scan progress |
system_status | Service connectivity changes (Spotify rate limit, slskd disconnect) |
activity | System activity feed entries |
wishlist_update | Wishlist item added, status changed, or removed |
automation_run | Automation started, completed, or failed |
All UI elements that show live progress (download bars, worker icons, scan counters) are driven by these WebSocket events. External clients can connect using any Socket.IO-compatible library and listen for these same events.
-The full API has 200+ endpoints covering library, downloads, playlists, automations, profiles, settings, and more. Use a reverse proxy (Nginx, Caddy, Traefik) for external access with HTTPS.
-All API v1 endpoints require an API key (except POST /api-keys/bootstrap). Generate keys in Settings → API Keys or via the bootstrap endpoint.
| Method | Format | Example |
|---|---|---|
| Header | Authorization: Bearer {key} | Authorization: Bearer sk_AbCd... |
| Query | ?api_key={key} | /api/v1/system/status?api_key=sk_AbCd... |
sk_ prefix. The raw key is shown exactly once at creation time. Only a SHA-256 hash is stored server-side. Rate limit: 60 requests per minute per IP.All endpoints are prefixed with /api/v1
Every response follows this structure:
'; + sectionsHTML += '| Status | Code | Meaning |
|---|---|---|
| 400 | BAD_REQUEST | Missing or invalid parameters |
| 401 | AUTH_REQUIRED | No API key provided |
| 403 | INVALID_KEY / FORBIDDEN | Invalid key or insufficient permissions |
| 404 | NOT_FOUND | Resource not found |
| 409 | CONFLICT | Resource already exists or action in progress |
| 429 | RATE_LIMITED | Too many requests |
| 500 | *_ERROR | Internal server error |
' + group.desc + '
'; + + group.endpoints.forEach(ep => { + const idx = globalIdx++; + endpointRegistry.push(ep); + + sectionsHTML += '' + ep.desc + '
'; + sectionsHTML += buildParamsTable(ep.params); + sectionsHTML += buildBodyTable(ep.bodyFields); + sectionsHTML += buildExample(ep.example); + sectionsHTML += buildTryIt(ep, idx); + sectionsHTML += 'SoulSync uses Socket.IO for real-time updates. Connect to the same host/port as the web UI. No API key required for WebSocket connections.
'; + sectionsHTML += '| Event | Description | Key Fields |
|---|---|---|
download_progress | Per-track download progress | title, percent, speed, eta |
download_complete | Track finished downloading | title, artist, album, file_path |
batch_progress | Album/playlist batch status | batch_id, completed, total, current_track |
worker_status | Enrichment worker updates | worker, status, matched, total, current |
scan_progress | Library/quality/duplicate scan | type, progress, total, current |
system_status | Service connectivity changes | service, connected, rate_limited |
activity | Activity feed entries | timestamp, type, message |
wishlist_update | Wishlist item changes | action, track_id, track_name |
automation_run | Automation execution events | automation_id, status, result |