// =============================== // HELP & DOCS PAGE // =============================== function docsImg(src, alt) { return `
SoulSync is a self-hosted music download, sync, and library management platform. It connects to Spotify, Apple Music/iTunes, Deezer, Tidal, Qobuz, YouTube, and Beatport for metadata, and downloads from Soulseek, YouTube, Tidal, Qobuz, HiFi, and Deezer. Your library is served through Plex, Jellyfin, or Navidrome.
${docsImg('gs-overview.jpg', 'SoulSync dashboard overview')}Search and download tracks in FLAC, MP3, and more from 6 sources (Soulseek, YouTube, Tidal, Qobuz, HiFi, Deezer), with automatic metadata tagging and file organization.
Mirror playlists from Spotify, YouTube, Tidal, and Beatport. Discover official metadata and sync to your media server.
Browse, edit, and enrich your music library with metadata from 9 services. Write tags directly to audio files.
Schedule tasks, chain workflows with signals, and get notified via Discord, Pushbullet, or Telegram.
Discover new artists via similar-artist recommendations, seasonal playlists, genre exploration, and time-machine browsing.
Follow artists and automatically scan for new releases. New tracks are added to your wishlist for download.
After launching SoulSync, head to the Settings page to configure your services. At minimum you need:
SoulSync integrates with many external services. Here's a quick reference for each:
| Service | Purpose | Auth Required |
|---|---|---|
| Spotify | Primary metadata source (artists, albums, tracks, cover art, genres) | OAuth — Client ID + Secret |
| iTunes / Apple Music | Fallback metadata source, always free, no auth needed | None |
| Soulseek (slskd) | Download source — P2P network, best for lossless and rare music | URL + API key |
| YouTube | Download source — audio extraction via yt-dlp | None (optional cookies browser) |
| Tidal | Download source + playlist import + enrichment | OAuth — Client ID + Secret |
| Qobuz | Download source + enrichment | Username + Password (app ID auto-fetched) |
| HiFi | Download source — free lossless via community API | None |
| Deezer | Download source + metadata fallback | ARL cookie token |
| Plex | Media server — library scanning, metadata sync, audio streaming | URL + Token |
| Jellyfin | Media server — library scanning, audio streaming | URL + API Key |
| Navidrome | Media server — auto-detects changes, audio streaming | URL + Username + Password |
| Last.fm | Enrichment — listener stats, tags, bios, similar artists | API Key |
| Genius | Enrichment — lyrics, descriptions, alternate names | Access Token |
| AcoustID | Audio fingerprint verification of downloads | API Key |
| ListenBrainz | Listening history and recommendations | URL + Token |
SoulSync uses a sidebar navigation layout. The left sidebar contains links to every page, a media player at the bottom, and service status indicators. The main content area changes based on the selected page.
Version & Updates: Click the version number in the sidebar footer to open the What's New modal, which shows detailed release notes for every feature and fix. SoulSync automatically checks for updates by comparing your running version against the latest GitHub commit. If an update is available, a banner appears in the modal. Docker users are notified when a new image has been pushed to the repo.
SoulSync uses three folders to manage your music files. Most setup issues come from incorrect folder configuration — especially in Docker. Read this section carefully.
docker-compose.yml — this makes folders accessible to the container.| Folder | Default (Docker) | Purpose |
|---|---|---|
| Download Path | /app/downloads | Where slskd/YouTube/Tidal/Qobuz initially saves downloaded files. This is a temporary staging area — files should not stay here permanently. |
| Transfer Path | /app/Transfer | Where post-processed files are moved after tagging and renaming. This must be the folder your media server (Plex/Jellyfin/Navidrome) monitors. |
| Staging Path | /app/Staging | For the Import feature only. Drop audio files here to import them into your library via the Import page. |
Artist/Album/01 - Title.flac)In Docker, every app runs in its own isolated container with its own filesystem. Volume mounts in docker-compose create "bridges" between your host folders and the container. But SoulSync doesn't automatically know where those bridges go — you have to tell it via the Settings page.
Here's what happens with a properly configured setup:
/mnt/data/slskd-downloads/ ← where slskd saves files on your server/mnt/media/music/ ← where Plex/Jellyfin/Navidrome watches/mnt/data/slskd-downloads:/app/downloads/mnt/media/music:/app/Transfer/app/downloads/ ← same files as /mnt/data/slskd-downloads//app/Transfer/ ← same files as /mnt/media/music//app/downloads/app/Transfer
Many users set up their docker-compose volumes correctly but never open SoulSync Settings to configure the paths. The app defaults may not match your volume mounts. You must go to Settings → Download Settings and verify that:
/app/downloads)/app/Transfer)The Download Path in SoulSync must point to the exact same physical folder where slskd saves its completed downloads. If they don't match, SoulSync can't find the files and post-processing fails silently.
/downloads/complete inside its own container- /mnt/data/slskd-downloads:/downloads/complete- /mnt/data/slskd-downloads:/app/downloads (same host folder!)/app/downloads/mnt/data/slskd-downloads). The container-internal paths can be different — that's fine. What matters is they point to the same physical directory on your server.
If you're running in Docker, the paths you enter in SoulSync's Settings page must be container-side paths (the right side of the : in your volume mount), not host paths (the left side). SoulSync runs inside the container and can only see its own filesystem.
| Setting Value | Result | |
|---|---|---|
| ✅ | /app/downloads | Correct — this is the container-side path (right side of :) |
| ✅ | /app/Transfer | Correct — this is the container-side path (right side of :) |
| ❌ | /mnt/data/slskd-downloads | Wrong — this is the host path (left side of :), doesn't exist inside the container |
| ❌ | /mnt/music | Wrong — host path, the container can't see this |
| ❌ | ./downloads | Wrong — relative path, use the full container path /app/downloads |
Your Transfer Path must ultimately point to the same physical directory your media server monitors. This is how new music appears in Plex/Jellyfin/Navidrome.
/mnt/media/music on the host- /mnt/media/music:/app/Transfer:rw/app/Transfer/app/Transfer inside the container → appears at /mnt/media/music on the host → Plex sees it and adds it to your library.
Here's a working example showing both slskd and SoulSync configured to share the same download folder:
# docker-compose.ymlservices: slskd: image: slskd/slskd:latest volumes: # slskd saves completed downloads here - /mnt/data/slskd-downloads:/downloads - /docker/slskd/config:/app soulsync: image: boulderbadgedad/soulsync:latest volumes: # SAME host folder as slskd — this is the key! - /mnt/data/slskd-downloads:/app/downloads # Your media server's music folder - /mnt/media/music:/app/Transfer:rw # Config, logs, staging, database - /docker/soulsync/config:/app/config - /docker/soulsync/logs:/app/logs - /docker/soulsync/staging:/app/Staging - soulsync_database:/app/data# Then in SoulSync Settings:# Download Path: /app/downloads# Transfer Path: /app/Transfer
Go through every item. If you miss any single one, the pipeline will break:
:) must be identical./app/Transfer with :rw permissions./app/downloads and Transfer Path to /app/Transfer (or whatever container paths you used on the right side of :).http://slskd:5030 or http://host.docker.internal:5030) and API key.id on your host. Set those values in docker-compose environment variables. Both slskd and SoulSync should use the same PUID/PGID.If paths are correct but files still won't transfer, it's usually a permissions issue. SoulSync needs read + write access to all three folders.
PUID and PGID in your docker-compose to match the user that owns your music folders (run id on your host to find your UID/GID — usually 1000/1000)chmod -R 755 /mnt/media/music (use your actual host path)Run these commands to confirm everything is wired up correctly:
docker exec soulsync-webui ls -la /app/downloads — you should see slskd's downloaded files here. If empty or "No such file or directory", your volume mount is wrong.docker exec soulsync-webui touch /app/Transfer/test.txt && echo "OK" — then check that test.txt appears in your media server's music folder on the host. Clean up after: rm /mnt/media/music/test.txtdocker exec soulsync-webui id — the uid and gid should match your PUID/PGID values./app/downloads), not host paths.logs/app.log for any path errors.| Symptom | Likely Cause | Fix |
|---|---|---|
| Files download but never transfer | App settings not configured — docker-compose volumes are set but SoulSync Settings still have defaults or wrong paths | Open Settings → Download Settings and set Download Path + Transfer Path to your container-side mount paths. |
| Post-processing log is empty | SoulSync can't find the downloaded file at the expected path — the Download Path in Settings doesn't match where slskd actually saves files inside the container | Run docker exec soulsync-webui ls /app/downloads to see what's actually there. The Download Path in Settings must match this path exactly. |
| Same tracks downloading multiple times | Post-processing fails so SoulSync thinks the track was never downloaded successfully. On resume, it tries again. | Fix the folder paths first. Once post-processing works, files move to Transfer and SoulSync knows they exist. |
| Files not renamed properly | Post-processing isn't running (path mismatch) or file organization is disabled in Settings | Verify File Organization is enabled in Settings → Processing & Organization. Fix Download Path first. |
| Permission denied in logs | Container user can't write to the Transfer folder on the host | Set PUID/PGID to match the host user that owns the music folder. Run chmod -R 755 on the Transfer host folder. |
| Media server doesn't see new files | Transfer Path doesn't map to the folder your media server monitors | Ensure the host path in your SoulSync volume mount (/mnt/media/music:/app/Transfer) is the same folder Plex/Jellyfin/Navidrome watches. |
| slskd downloads work fine on their own but not through SoulSync | slskd's download folder and SoulSync's Download Path point to different physical locations | Both containers must mount the same host directory. Check the left side of : in both docker-compose volume entries — they must match. |
logs/app.log. The post-processing log will show exactly where the file pipeline breaks — whether it's a path not found, permission denied, or verification failure. If the post-processing log is empty, the issue is almost certainly a path mismatch (SoulSync never found the file to process).SoulSync runs in Docker with the following environment variables:
| Variable | Default | Description |
|---|---|---|
DATABASE_PATH | ./database | Directory where the SQLite database is stored. Mount a volume here to persist data across container restarts. |
SOULSYNC_CONFIG_PATH | ./config | Directory where config.json and the encryption key are stored. Mount a volume here to persist settings. |
SOULSYNC_COMMIT_SHA | (auto) | Baked in at Docker build time. Used for update detection — compares against GitHub's latest commit. |
Your docker-compose volumes section must include these mappings. The left side is your host path, the right side is where SoulSync sees it inside the container:
| Mount | Container Path | What Goes Here |
|---|---|---|
| slskd downloads | /app/downloads | Must be the same physical folder slskd writes completed downloads to. Both containers mount the same host directory. |
| Music library | /app/Transfer | Your media server's monitored music folder. Add :rw to ensure write access. |
| Staging | /app/Staging | (Optional) For the Import feature — drop files here to import them. |
| Config | /app/config | Stores config.json and encryption key. Persists settings across restarts. |
| Logs | /app/logs | Application logs including app.log and post-processing.log. |
| Database | /app/data | Must use a named volume (not a host path). Host path mounts can cause database corruption. |
soulsync_database:/app/data), never a host path mount. Host path mounts can cause SQLite corruption, especially on networked file systems or when permissions don't align.Podman / Rootless Docker: SoulSync supports Podman rootless (keep-id) and rootless Docker setups. The entrypoint handles permission alignment automatically.
Config migration: When upgrading from older versions, SoulSync automatically migrates settings from config.json to the database on first startup. No manual migration is needed.
SoulSync can do a lot, but you don't need to learn everything at once. Here are the 6 essential workflows that cover 90% of what most users need. Start with whichever one matches your goal, and explore the rest later.
Search for any album, pick your tracks, and download in FLAC or MP3 with full metadata.
View Guide →Import your Spotify playlists and download every track to your local library.
View Guide →Follow your favorite artists and automatically download their new releases.
View Guide →Bring your existing music files into SoulSync with proper tags and organization.
View Guide →Link Plex, Jellyfin, or Navidrome so downloads appear in your library automatically.
View Guide →Once connected, do these 5 things to get the most out of SoulSync right away.
See below ↓Goal: Find an album and download it to your library with full metadata, cover art, and proper file organization.
Prerequisites: At least one download source connected (Soulseek, YouTube, Tidal, or Qobuz). Download and Transfer paths configured.
Result: Tracks appear in your Transfer folder as Artist/Album/01 - Title.flac and your media server is notified to scan.
Goal: Import a Spotify playlist and download all its tracks to your local library.
Goal: Automatically download new releases from your favorite artists without manual intervention.
Goal: Bring music files you already have into SoulSync with proper metadata and organization.
Artist - Album/) in the Staging path configured in SettingsGoal: Link your media server so downloaded music automatically appears in your library and can be streamed via the built-in player.
The dashboard is your command center. At the top you'll see service status indicators for Spotify, your media server, and Soulseek — showing connected/disconnected state at a glance. Below that, stat cards display your library totals: artists, albums, tracks, and total library size.
Stats update in real-time via WebSocket — no page refresh needed.
${docsImg('dash-overview.jpg', 'Dashboard overview')}The header bar contains enrichment worker icons for each metadata service. Hover over any icon to see its current status, what item it's processing, and progress counts (e.g., "142/500 matched").
Workers run automatically in the background, enriching your library with metadata from:
Artist genres, follower counts, images, album release dates, track preview URLs
MBIDs for artists, albums, and tracks — enables accurate cross-referencing
Deezer IDs, genres, album metadata
Artist descriptions, artist art, album info
iTunes/Apple Music IDs, preview links
Listener/play counts, bios, tags, similar artists for every artist/album/track
Lyrics, descriptions, alternate names, song artwork
Tidal IDs, artist images, album labels, explicit flags, ISRCs
Qobuz IDs, artist images, album labels, genres, explicit flags
Rate Limit Protection: Workers include smart rate limiting for all APIs. If Spotify returns a rate limit with a Retry-After greater than 60 seconds, the app seamlessly switches to iTunes/Apple Music — an amber indicator appears in the sidebar, searches automatically use Apple Music, and the enrichment worker pauses. When the ban expires, everything recovers automatically. No action needed from the user.
The dashboard features several tool cards for library maintenance:
| Tool | What It Does |
|---|---|
| Database Updater | Refreshes your library by scanning your media server. Choose incremental (new only) or full refresh. |
| Metadata Updater | Triggers all 9 enrichment workers to re-check your library against all connected services. |
| Quality Scanner | Scans library for tracks below your quality preferences. Shows how many meet standards and finds replacements. |
| Duplicate Cleaner | Identifies and removes duplicate tracks from your library, freeing up disk space. |
| Discovery Pool | View and fix matched/failed discovery results across all mirrored playlists. |
| Retag Tool | Batch retag downloaded files with correct album metadata from Spotify/iTunes. |
| Backup Manager | Create, download, restore, and delete database backups. Rolling cleanup keeps the 5 most recent. |
The Retag Tool lets you fix incorrect metadata tags on files already in your library. This is useful when files were downloaded with wrong or incomplete tags.
The retag operation writes title, artist, album artist, album, track number, disc number, year, and genre. Cover art can optionally be re-embedded.
The Backup Manager protects your SoulSync database (all library data, watchlists, playlists, automations, and settings).
The system automation Auto-Backup Database creates a backup every 3 days automatically. You can adjust the interval in Automations.
Additional maintenance tools accessible from the dashboard:
The activity feed at the bottom of the dashboard shows recent system events: downloads completed, syncs started, settings changed, automation runs, and errors. Events appear in real-time via WebSocket.
Events include: downloads started/completed/failed, playlist syncs, watchlist scans, automation runs, enrichment worker progress, settings changes, and system errors. The feed shows the 10 most recent events and updates in real-time via WebSocket. Older events are available in the application logs.
The Sync page lets you import playlists from Spotify, YouTube, Tidal, and Beatport. Once imported, playlists are mirrored — they persist in your SoulSync instance and can be refreshed, discovered, and synced to your wishlist for downloading.
${docsImg('sync-overview.jpg', 'Playlist sync page')}If Spotify is connected, click Refresh to load all your Spotify playlists. Each playlist shows its cover art, track count, and sync status.
For each playlist you can:
Paste a YouTube playlist URL into the input field and click Parse Playlist. SoulSync extracts the track list and attempts to match each track to official Spotify/iTunes metadata.
${docsImg('sync-youtube.jpg', 'YouTube playlist import')}Requires Tidal authentication in Settings. Once connected, refresh to load your Tidal playlists. You can also select Tidal download quality: HQ (320kbps), HiFi (FLAC 16-bit), or HiFi Plus (up to 24-bit).
If ListenBrainz is configured in Settings, the Sync page includes a ListenBrainz tab for browsing and importing playlists from your ListenBrainz account:
ListenBrainz tracks are matched against Spotify/iTunes using a 4-strategy search: direct match, swapped artist/title, album-based lookup, and extended fuzzy search. Discovered tracks can be synced to your library like any other playlist.
The Beatport tab provides deep integration with electronic music content across three views:
Browse — Featured content organized into sections:
Genre Browser — Browse 12+ electronic music genres (House, Techno, Drum & Bass, Trance, etc.) with per-genre views: Top 10 tracks, staff picks, hype rankings, latest releases, and new charts.
Charts — Top 100 and Hype charts with full track listings. Each track can be manually matched against Spotify for metadata, then synced and downloaded.
${docsImg('sync-beatport.jpg', 'Beatport genre browser')}Sync Spotify playlists and albums without OAuth credentials. Paste any public Spotify playlist or album URL and SoulSync will load the tracks for download. Useful when you don't want to connect a Spotify account or want to sync from someone else's public playlist.
open.spotify.com/playlist/... or open.spotify.com/album/... URLImport Deezer playlists by URL. Paste a Deezer playlist URL, click Load Playlist, and SoulSync parses the tracks for discovery and download. Tracks go through the same discovery pipeline as YouTube and Tidal playlists.
deezer.com/playlist/... URLImport track lists from CSV, TSV, or plain text files. Drag and drop a file or click to browse. SoulSync parses the file, lets you preview and map columns, then creates a mirrored playlist for discovery and download.
View a log of all sync operations. The Sync History button in the page header opens a modal showing every playlist sync, album download, and wishlist processing operation with timestamps, track counts, and completion status.
Every parsed playlist from any source is automatically mirrored. The Mirrored tab shows all saved playlists with source-branded cards, live discovery status, and download progress.
Export any mirrored playlist as an M3U file for use in external media players or media servers. Enable M3U export in Settings and use the export button on any playlist card.
M3U files reference the actual file paths in your library, so they work with any M3U-compatible player.
Auto-Save — When enabled in Settings, M3U files are automatically regenerated every time a playlist is synced or updated. Manual Export — The export button on any playlist modal creates an M3U file on demand, even when auto-save is disabled.
For non-Spotify playlists (YouTube, Tidal), tracks need to be discovered before syncing. Discovery matches raw titles to official Spotify/iTunes metadata using fuzzy matching with a 0.7 confidence threshold.
The default search mode. Type an artist, album, or track name and results appear in a categorized dropdown: In Your Library, Artists, Albums, Singles & EPs, and Tracks. Results come from your primary metadata source (Spotify by default).
Toggle to Basic Search mode for direct Soulseek queries. This shows raw search results with detailed info: format, bitrate, quality score, file size, uploader name, upload speed, and availability.
Filters let you narrow results by type (Albums/Singles), format (FLAC/MP3/OGG/AAC/WMA), and sort by relevance, quality, size, bitrate, duration, or uploader speed.
${docsImg('dl-basic-search.jpg', 'Basic Soulseek search')}SoulSync supports multiple download sources, configurable in Settings → Download Settings:
| Source | Description | Best For |
|---|---|---|
| Soulseek | P2P network via slskd — largest selection of lossless and rare music | FLAC, rare tracks, DJ sets |
| YouTube | YouTube audio extraction via yt-dlp | Live performances, remixes, tracks not on Soulseek |
| Tidal | Tidal HiFi streaming rip (requires auth) | Guaranteed quality, official releases |
| Qobuz | Qobuz Hi-Res streaming rip (requires auth) | Audiophile quality, up to 24-bit/192kHz |
| HiFi | Free lossless downloads via community-run API instances | No account needed, good FLAC availability |
| Deezer | Deezer streaming rip via ARL token (FLAC/MP3) | Large catalog, easy setup, FLAC with HiFi sub |
| Hybrid | Tries your primary source first, then automatically falls back to alternates | Best overall success rate |
YouTube settings include cookies browser selection (for bot detection bypass), download delay (seconds between requests), and minimum confidence threshold for title matching.
When you select an album or track to download, a modal appears with:
Downloads can be started from multiple places: Enhanced Search results, artist discography, Download Missing modal, wishlist auto-processing, and playlist sync.
Download Candidate Selection: If a download fails or no suitable source is found, you can view the cached search candidates and manually pick an alternative file from a different user. This lets you recover failed downloads without restarting the entire search.
${docsImg('dl-candidates.jpg', 'Download candidate selection')}After a file is downloaded, it goes through an automatic pipeline before appearing in your library:
$artist, $album, $title, $track, $year, $quality, and $albumtype (resolves to Album, Single, EP, or Compilation). For multi-disc albums, a Disc N/ subfolder is automatically created when the album has more than one disc (or use $disc in your template for manual control)..lrc sidecar files alongside the audio file. Compatible media players (foobar2000, MusicBee, Plex, etc.) will display time-synced lyrics automatically. Falls back to plain-text lyrics if synced versions aren't available.Configure your quality preferences in Settings → Quality Profile. Quick presets:
| Preset | Priority |
|---|---|
| Audiophile | FLAC first, then MP3 320 |
| Balanced | MP3 320 first, then FLAC, then MP3 256 |
| Space Saver | MP3 256 first, then MP3 192 |
Each format has configurable bitrate ranges and a priority order. Enable Fallback to accept any quality when preferred formats aren't available.
Toggle the download manager panel (right sidebar) to see all active and completed downloads. Each download shows real-time progress: track name, format, speed, ETA, and a cancel button. Use Clear Completed to clean up finished items.
The hero slider showcases recommended artists based on your watchlist. Each slide shows the artist's image, name, popularity score, genres, and similarity context. Use the arrows or dots to navigate, or click:
SoulSync generates playlists from two sources: your discovery pool (50 similar artists refreshed during watchlist scans) and your library listening data:
| Playlist | Source | Description |
|---|---|---|
| Popular Picks | Discovery Pool | Top tracks from discovery pool artists |
| Hidden Gems | Discovery Pool | Rare and deeper cuts from pool artists |
| Discovery Shuffle | Discovery Pool | Randomized mix across all pool artists |
| Recently Added | Library | Tracks most recently added to your collection |
| Top Tracks | Library | Your most-played or highest-rated tracks |
| Forgotten Favorites | Library | Tracks you haven't listened to in a while |
| Decade Mixes | Library | Tracks grouped by release decade (70s, 80s, 90s, etc.) |
| Daily Mixes | Library | Auto-generated daily playlists based on your taste profile |
| Familiar Favorites | Library | Well-known tracks from artists you follow |
Each playlist can be played in the media player, downloaded, or synced to your media server.
Genre Browser — Filter discovery pool content by specific genres. Browse available genres and view top tracks within each genre category.
${docsImg('disc-genre-browser.jpg', 'Genre browser')}ListenBrainz Playlists — If ListenBrainz is configured, the Discover page also shows personalized playlists generated from your listening history: Created For You, Your Playlists, and Collaborative playlists.
Search for 1–5 artists, select them, and click Generate to create a custom playlist from their catalogs. You can then download or sync the generated playlist.
${docsImg('disc-build-playlist.jpg', 'Build custom playlist')}The Discover page includes auto-generated seasonal content based on the current time of year, plus two curated sections:
Both can be synced to your media server with live progress tracking.
Browse discovery pool content by decade — tabs from the 1950s through the 2020s. Each decade pulls top tracks from pool artists active in that era.
${docsImg('disc-time-machine.jpg', 'Time Machine decade browser')}Search for any artist by name. Results show artist cards with images and genres. Results come from Spotify (or iTunes as fallback). Click any card to open the artist detail view.
${docsImg('art-search.jpg', 'Artist search results')}The artist detail page shows a full discography organized by category:
At the top, View on buttons link to the artist on each matched external service (Spotify, Apple Music, MusicBrainz, Deezer, AudioDB, Last.fm, Genius, Tidal, Qobuz). Service badges on artist cards also indicate which services have matched this artist.
Similar Artists appear as clickable bubbles below the discography for further exploration and discovery.
${docsImg('art-detail.jpg', 'Artist detail page')}The watchlist tracks artists you want to follow for new releases. When SoulSync scans your watchlist, it checks each artist's discography and adds any new tracks to your wishlist for downloading.
Click Scan for New Releases or let the system automation handle it (runs every 24 hours). The scan shows a live activity panel with:
The wishlist is the queue of tracks waiting to be downloaded. Tracks are added to the wishlist from multiple sources:
Auto-Processing: The system automation runs every 30 minutes, picking up wishlist items and attempting to download them from your configured source. Processing alternates between album and singles cycles — one run processes albums, the next run processes singles. If one category is empty, it automatically switches to the other. Failed items are retried with increasing backoff.
Manual Processing: Use the Process Wishlist automation action to trigger processing on demand. Options include processing all items, albums only, or singles only.
Cleanup: The Cleanup Wishlist action removes duplicates (same track added multiple times) and items you already own in your library.
Per-Artist Settings — Click the config icon on any watched artist to customize what release types to include: Albums, EPs, Singles, Live versions, Remixes, Acoustic versions, Compilations.
Global Settings — Override all per-artist settings at once. Enable Global Override, select which types to include, and all watchlist scans will follow the global config.
Automations let you schedule tasks and react to events with a visual WHEN → DO → THEN builder. Create custom workflows like "When a download completes, update the database, then notify me on Discord."
Each automation card shows its trigger/action flow, last run time, next scheduled run (with countdown), and a Run Now button for instant execution.
${docsImg('auto-overview.jpg', 'Automations page')}Click + New Automation to open the builder. Drag or click blocks from the sidebar into the three slots:
Add Conditions to filter when the automation runs. Match modes: All (AND) or Any (OR). Operators: contains, equals, starts_with, not_contains.
${docsImg('auto-builder.jpg', 'Automation builder')}| Trigger | Description |
|---|---|
| Schedule | Run on a timer interval (minutes/hours/days) |
| Daily Time | Run every day at a specific time |
| Weekly Time | Run on specific weekdays at a set time |
| App Started | Fires when SoulSync starts up |
| Track Downloaded | When a track finishes downloading |
| Download Failed | When a track permanently fails to download |
| Download Quarantined | When AcoustID verification rejects a download |
| Batch Complete | When an album/playlist batch download finishes |
| Wishlist Item Added | When a track is added to the wishlist |
| Wishlist Processing Done | When auto-wishlist processing finishes |
| New Release Found | When a watchlist scan finds new music |
| Watchlist Scan Done | When the full watchlist scan completes |
| Artist Watched/Unwatched | When an artist is added to or removed from the watchlist |
| Playlist Synced | When a playlist sync completes |
| Playlist Changed | When a mirrored playlist detects changes from the source |
| Discovery Complete | When playlist track discovery finishes |
| Library Scan Done | When a media library scan finishes |
| Database Updated | When a library database refresh finishes |
| Quality Scan Done | When quality scan finishes (with counts of quality met vs low quality) |
| Duplicate Scan Done | When duplicate cleaner finishes (with files scanned, duplicates found, space freed) |
| Import Complete | When an album/track import finishes |
| Playlist Mirrored | When a new playlist is mirrored for the first time |
| Signal Received | Custom signal fired by another automation |
| Action | Description |
|---|---|
| Process Wishlist | Retry failed downloads (all, albums only, or singles only) |
| Scan Watchlist | Check watched artists for new releases |
| Cleanup Wishlist | Remove duplicate/owned tracks from wishlist |
| Scan Library | Trigger a media server library scan |
| Update Database | Refresh library database (incremental or full) |
| Deep Scan Library | Full library comparison without losing enrichment data |
| Refresh Mirrored Playlist | Re-fetch playlist tracks from the source |
| Sync Playlist | Sync a specific playlist to your media server |
| Discover Playlist | Find official metadata for playlist tracks |
| Run Duplicate Cleaner | Scan for and remove duplicate files |
| Run Quality Scan | Scan for low-quality audio files |
| Clear Quarantine | Delete all quarantined files |
| Update Discovery | Refresh the discovery artist pool |
| Backup Database | Create a timestamped database backup |
| Refresh Beatport Cache | Scrape Beatport homepage and warm the data cache |
| Clean Search History | Remove old searches from Soulseek (keeps 50 most recent) |
| Clean Completed Downloads | Clear completed downloads and empty directories from the download folder |
| Full Cleanup | Clear quarantine, download queue, staging folder, and search history in one sweep |
| Notify Only | No action — just trigger notifications |
After the DO action completes, up to 3 THEN actions run:
All notification messages support variable substitution: {name}, {status}, {time}, {run_count}, and context-specific variables from the action result.
Test Notifications: Use the test button next to any notification then-action to send a test message before saving. This verifies your webhook URL, API key, or bot token is working correctly.
Each automation card shows its last run time and run count. For scheduled automations, a countdown timer shows when the next run will occur.
Use the Run Now button on any automation card to execute it immediately, regardless of its schedule. The result (success/failure) updates in real-time on the card. Running automations display a glow effect on their card.
Stall detection: If an automation action runs for more than 2 hours without completing, it is automatically flagged as stalled and terminated to prevent resource leaks.
The Dashboard activity feed also logs every automation execution with timestamps, so you can review the full history of what ran and when.
${docsImg('auto-history.jpg', 'Automation execution history')}SoulSync ships with 10 built-in automations that handle routine maintenance. You can enable/disable them and modify their configs, but you can't delete them or rename them.
| Automation | Schedule |
|---|---|
| Auto-Process Wishlist | Every 30 minutes |
| Auto-Scan Watchlist | Every 24 hours |
| Auto-Scan After Downloads | On batch_complete event |
| Auto-Update Database | On library_scan_completed event |
| Refresh Beatport Cache | Every 24 hours |
| Clean Search History | Every 1 hour |
| Clean Completed Downloads | Every 5 minutes |
| Auto-Deep Scan Library | Every 7 days |
| Auto-Backup Database | Every 3 days |
| Full Cleanup | Every 12 hours |
The Library page shows all artists in your collection as cards with images, album/track counts, and service badges (Spotify, MusicBrainz, Deezer, AudioDB, iTunes, Last.fm, Genius, Tidal, Qobuz) indicating which services have matched this artist.
Use the search bar, alphabet navigation (A–Z, #), and watchlist filter (All/Watched/Unwatched) to browse. Click any artist card to view their discography.
The artist detail page shows albums, EPs, and singles as cards with completion percentages. Filter by category, content type (live/compilations/featured), or status (owned/missing). At the top, View on buttons link to the artist on each matched external service.
${docsImg('lib-standard.jpg', 'Library artist grid')}Toggle Enhanced on any artist's detail page to access the professional library management tool. This view is admin-only — non-admin profiles see the Standard view only.
In the Enhanced view, each artist, album, and track shows match status chips for all 9 services. Click any chip to manually search and link the correct external ID. Run per-service enrichment from the dropdown to pull in metadata from a specific source.
Matched services show as clickable badges linking to the entity on that service's website.
Sync your database metadata to actual audio file tags:
Supports MP3, FLAC, OGG, and M4A via Mutagen. After writing, optional server sync pushes metadata to Plex (per-track update), Jellyfin (library scan), or Navidrome (auto-detects).
Select tracks across multiple albums using the checkboxes. The bulk bar appears showing the selection count with actions:
From any album card showing missing tracks, click Download Missing to open a modal listing all tracks not in your library. Select tracks, choose a download source, and start the download. Progress is tracked per-track with status indicators.
Multi-Disc Albums: Albums with multiple discs are handled automatically. Tracks are organized into Disc N/ subfolders within the album directory, preventing track number collisions (e.g., Disc 1 Track 1 vs Disc 2 Track 1). The disc structure is detected from Spotify or iTunes metadata.
Set your staging folder path in Settings → Download Settings. Place audio files you want to import into this folder. SoulSync scans the folder and detects albums from the file structure.
Place albums in subfolders (e.g., Artist - Album/) and loose singles at the root level.
The import page header shows the total files in staging and their combined size.
${docsImg('imp-staging.jpg', 'Import staging page')}The Singles tab handles individual tracks that aren't part of an album structure. Files in the staging root (not in subfolders) appear here. Search for the correct track on Spotify/iTunes, confirm the match, and import. The file is tagged, renamed, and placed in your library.
The import matching system compares staged files against official album track lists:
After matching, the import process tags files with the official metadata (title, artist, album, track number, cover art) and moves them to your transfer path following the standard file organization template.
Import track lists from CSV, TSV, or TXT files. Upload a file with columns for artist, album, and track title:
The sidebar media player is always visible when a track is loaded. It shows album art, track info, a seekable progress bar, and playback controls (play/pause, previous, next, volume, repeat, shuffle).
${docsImg('player-sidebar.jpg', 'Sidebar media player')}Click the sidebar player to open the Now Playing modal — a full-screen experience with large album art, ambient glow (dominant color from cover art), a frequency-driven audio visualizer, and expanded controls.
${docsImg('player-nowplaying.jpg', 'Now Playing modal')}The media player streams audio directly from your connected media server — no local file access needed:
The browser auto-detects which audio formats it can play. Album art, track metadata, and ambient colors are all pulled from your server in real-time.
Add tracks to the queue from the Enhanced Library Manager or download results. Manage the queue in the Now Playing modal: reorder, remove individual tracks, or clear all.
Smart Radio mode (toggle in queue header) automatically adds similar tracks when the queue runs out, based on genre, mood, style, and artist similarity. Playback continues seamlessly.
Repeat modes: Off → Repeat All (loop queue) → Repeat One. Shuffle randomizes the next track from the remaining queue.
${docsImg('player-queue.jpg', 'Queue panel')}| Key | Action |
|---|---|
| Space | Play / Pause |
| → | Seek forward / Next track |
| ← | Seek backward / Previous track |
| ↑ | Volume up |
| ↓ | Volume down |
| M | Mute / Unmute |
| Escape | Close Now Playing modal |
Media Session API — SoulSync integrates with your OS media controls (lock screen, system tray) for play/pause, next/previous, and seek.
Configure credentials for each external service. All fields are saved to your local database and encrypted at rest using a Fernet key generated on first launch. Nothing is sent to external servers except during actual API calls. Each service has a Test Connection button to verify your credentials are working.
arl)Connect your media server so SoulSync can scan your library, trigger updates, stream audio, and sync metadata:
| Server | Credentials | Setup Details |
|---|---|---|
| Plex | URL + Token | After connecting, select which Music Library to use from the dropdown. SoulSync scans this library for your collection and triggers scans after downloads. |
| Jellyfin | URL + API Key | Select the User and Music Library to target. SoulSync uses the Jellyfin API for library scans and can stream audio directly. |
| Navidrome | URL + Username + Password | Select the Music Folder to monitor. Navidrome auto-detects new files, so SoulSync doesn't need to trigger scans — just place files in the right folder. |
The media player streams audio directly from your connected server — tracks play through your Plex, Jellyfin, or Navidrome instance without needing local file access.
/app/downloads), not the host path. SoulSync monitors this folder for completed downloads to begin post-processing./app/Transfer)./app/downloads, /app/Transfer). Never use host paths like /mnt/music — the container can't access those. Your docker-compose volumes section is where host paths are mapped to container paths. See Getting Started → Folder Setup for a complete walkthrough.Control how downloaded files are processed and organized:
{artist}, {album}, {title}, {track_number}, {year}, {genre}. Default: {artist}/{album}/{track_number} - {title}Disc 1/, Disc 2/, etc.Set your preferred audio quality with presets (Audiophile/Balanced/Space Saver) or custom configuration per format. Each format has a configurable bitrate range and priority order. Enable Fallback to accept any quality when nothing matches.
sk_ prefix and are shown once at creation — only a SHA-256 hash is stored for security.Artist/Album/TrackNum - Title.extSoulSync supports Netflix-style multiple profiles for shared households. Each profile gets its own:
Shared across all profiles: Music library (files and metadata), service credentials, settings, and automations.
PINs are 4-6 digits. If you forget your PIN, the admin can reset it from Manage Profiles. The admin PIN protects settings and destructive operations when multiple profiles exist.
${docsImg('profiles-picker.jpg', 'Profile picker')} ${docsImg('profiles-create.jpg', 'Profile creation')}Admins can control what each profile has access to. When creating or editing a non-admin profile:
If the admin removes a page that was set as a user's home page, the home page automatically resets. Navigation guards prevent users from accessing restricted pages even via direct URL or browser history.
Each user can choose which page they land on when they log in:
?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}'
})
]
}
];
// --- Build endpoint HTML ---
function methodClass(m) { return m.toLowerCase(); }
function buildParamsTable(params) {
if (!params || !params.length) return '';
let html = '| Name | Type | Required | Description |
|---|---|---|---|
| ' + p.name + ' | ' + p.type + ' | ' + req + ' | ' + p.desc + def + ' |
| Field | Type | Required | Description |
|---|---|---|---|
| ' + p.name + ' | ' + p.type + ' | ' + req + ' | ' + p.desc + def + ' |
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 |