Subsonic API compatibility
VibeSonic implements the complete original Subsonic REST API through
version 1.16.1 (Phase 2) plus a growing set of OpenSubsonic
extensions (Phase 3). Every endpoint below is backed by real data
from VibeSonic's own database/scanner/streaming/transcoding/jukebox/
audio-analysis subsystems — none of them are stub handlers that return
an empty successful response. getOpenSubsonicExtensions only ever
advertises an extension once its handler and tests exist and pass; see
OpenSubsonic extensions below for the full
list and what backs each one.
How to read this table
- Version — the Subsonic API version the endpoint was introduced in
(per the official protocol history), included so client compatibility
checks make sense.
- Implemented — ✅ real, ⚠️ real but with a noted simplification.
- XML / JSON Tested — both formats share one Go struct tree
(
internal/subsonic/model*.go) serialized via encoding/xml and
encoding/json respectively, so a test that decodes one and asserts
on response shape effectively covers both; most endpoint tests
exercise JSON responses directly and XML responses via the shared
decodeXML/decodeJSON test helpers in internal/subsonic/*_test.go.
- Auth Tested — covered by
internal/subsonic/router_test.go
(wrong password, missing credentials, disabled user, token auth,
enc: password auth, external-auth-only accounts, protocol version
negotiation) plus per-endpoint auth-failure cases where relevant.
- Permission Tested — covered where the endpoint enforces a specific
Subsonic role (stream/download/podcast/jukebox/admin/ownership).
System
| Endpoint |
Version |
Implemented |
XML Tested |
JSON Tested |
Auth Tested |
Permission Tested |
Notes |
ping |
1.0.0 |
✅ |
✅ |
✅ |
✅ |
— |
|
getLicense |
1.0.0 |
✅ |
✅ |
✅ |
✅ |
— |
Always reports a valid, unlimited license — VibeSonic never requires one. |
Music folders
| Endpoint |
Version |
Implemented |
XML Tested |
JSON Tested |
Auth Tested |
Permission Tested |
Notes |
getMusicFolders |
1.0.0 |
✅ |
✅ |
✅ |
✅ |
✅ |
Respects per-user folder restrictions; folder IDs are small sequential integers derived from creation order (the protocol's musicFolder.id is xs:int), mapped internally onto VibeSonic's opaque folder IDs. |
Browsing
| Endpoint |
Version |
Implemented |
XML Tested |
JSON Tested |
Auth Tested |
Permission Tested |
Notes |
getIndexes |
1.0.0 |
✅ |
✅ |
— |
✅ |
✅ (folder restriction) |
Filesystem-style browsing, driven entirely by media_files.relative_path — never faked from the ID3 model. |
getMusicDirectory |
1.0.0 |
✅ |
✅ |
— |
✅ |
✅ (folder restriction) |
Directory IDs encode (folder, path); rejects a request for a folder the user can't see. |
getGenres |
1.9.0 |
✅ |
— |
✅ |
✅ |
— |
Real per-genre song/album counts. |
getArtists |
1.8.0 |
✅ |
— |
✅ |
✅ |
— |
ID3 browsing; only artists with at least one album, per protocol convention. |
getArtist |
1.8.0 |
✅ |
— |
✅ |
✅ |
— |
|
getAlbum |
1.8.0 |
✅ |
✅ |
✅ |
✅ |
✅ (folder restriction on songs) |
|
getSong |
1.8.0 |
✅ |
— |
✅ |
✅ |
✅ (folder restriction) |
|
getVideos |
1.8.0 |
✅ |
— |
✅ |
— |
— |
Real: lists media files the scanner tagged kind=video. |
getVideoInfo |
1.14.0 |
⚠️ |
— |
— |
— |
— |
Reports one default audio track (VibeSonic doesn't probe multi-audio-track containers yet) and real sidecar .srt/.vtt/.ass captions found next to the file. |
getArtistInfo |
1.11.0 |
✅ |
— |
✅ |
— |
— |
Local MusicBrainz ID always returned; biography (MusicBrainz disambiguation) filled in only when VIBESONIC_ENABLE_EXTERNAL_METADATA=true and the artist has a tagged MBID — see OpenSubsonic extensions. |
getArtistInfo2 |
1.11.0 |
✅ |
— |
✅ |
— |
— |
Same as above, ID3 form. |
getAlbumInfo |
1.14.0 |
✅ |
— |
✅ |
— |
— |
Local MusicBrainz ID always returned; cover-art image URLs filled in from the Cover Art Archive under the same conditions as getArtistInfo. |
getAlbumInfo2 |
1.14.0 |
✅ |
— |
✅ |
— |
— |
Same. |
getSimilarSongs |
1.11.0 |
✅ |
— |
— |
— |
— |
Genre-overlap candidate set (song/album/artist seed), re-ranked by real sonic-feature distance when the seed song has been audio-analyzed. Not the same as getSonicSimilarTracks (pure sonic search) — see below. |
getSimilarSongs2 |
1.11.0 |
✅ |
— |
— |
— |
— |
ID3 (artist-seeded) form. |
getTopSongs |
1.13.0 |
✅ |
— |
✅ |
— |
— |
Ranked by real aggregate play count across all users. |
Album/song lists
| Endpoint |
Version |
Implemented |
XML Tested |
JSON Tested |
Auth Tested |
Permission Tested |
Notes |
getAlbumList |
1.2.0 |
✅ |
— |
✅ |
✅ |
— |
All 10 official types: random, newest, highest, frequent, recent, alphabeticalByName, alphabeticalByArtist, starred, byYear, byGenre. |
getAlbumList2 |
1.8.0 |
✅ |
✅ |
✅ |
✅ |
— |
Same types, ID3 form. |
getRandomSongs |
1.2.0 |
✅ |
— |
✅ |
— |
— |
Supports size, genre, fromYear, toYear. |
getSongsByGenre |
1.9.0 |
✅ |
— |
✅ |
— |
— |
Supports count/offset. |
getNowPlaying |
1.0.0 |
✅ |
— |
✅ |
— |
— |
Real now_playing table, populated by scrobble (submission=false) and native web streaming. |
getStarred |
1.8.0 |
✅ |
— |
✅ |
— |
— |
|
getStarred2 |
1.8.0 |
✅ |
— |
✅ |
— |
— |
|
Search
| Endpoint |
Version |
Implemented |
XML Tested |
JSON Tested |
Auth Tested |
Permission Tested |
Notes |
search |
1.0.0 (deprecated 1.4.0) |
✅ |
— |
✅ |
— |
— |
Still fully functional, as required — deprecated doesn't mean removed. |
search2 |
1.4.0 |
✅ |
— |
✅ |
— |
— |
Independent offset/count per artist/album/song. |
search3 |
1.8.0 |
✅ |
— |
✅ |
✅ |
— |
ID3 form; independent offset/count per category. |
Playlists
| Endpoint |
Version |
Implemented |
XML Tested |
JSON Tested |
Auth Tested |
Permission Tested |
Notes |
getPlaylists |
1.0.0 |
✅ |
— |
✅ |
— |
— |
Own + public playlists; username param for admins to view another user's. |
getPlaylist |
1.0.0 |
✅ |
— |
✅ |
— |
✅ (ownership/public) |
|
createPlaylist |
1.0.0 |
✅ |
— |
✅ |
— |
✅ (playlist role) |
Supports both "new playlist" and "replace an existing playlist's songs" forms. |
updatePlaylist |
1.8.0 |
✅ |
— |
— |
— |
✅ (ownership) |
Rename, re-describe, publicity toggle, songIndexToRemove/songIdToAdd. |
deletePlaylist |
1.0.0 |
✅ |
— |
— |
— |
✅ (ownership) |
|
Media retrieval
| Endpoint |
Version |
Implemented |
XML Tested |
JSON Tested |
Auth Tested |
Permission Tested |
Notes |
stream |
1.0.0 |
✅ |
n/a (binary) |
n/a |
✅ |
✅ (stream role) |
Direct streaming (range requests, no buffering) or real ffmpeg transcoding depending on format/maxBitRate; folder-permission checked. |
download |
1.0.0 |
✅ |
n/a |
n/a |
— |
✅ (download role) |
Original bytes with Content-Disposition: attachment. |
hls |
1.8.0 |
✅ |
n/a |
n/a |
— |
✅ (stream role) |
Real ffmpeg HLS segmenting, cached per (source, bitrate); segment URLs re-embed the caller's auth params. |
getCaptions |
1.14.0 |
✅ |
n/a |
n/a |
— |
— |
Serves real sidecar subtitle files. |
getCoverArt |
1.0.0 |
✅ |
n/a |
n/a |
— |
✅ (coverArt role) |
Embedded or folder art, resized and cached on disk. |
getLyrics |
1.2.0 |
✅ |
✅ |
— |
— |
— |
Title/artist search over the same structured lyrics storage getLyricsBySongId reads; empty (not fabricated) if none found. |
getAvatar |
1.8.0 |
⚠️ |
— |
— |
— |
— |
Honestly 404s — VibeSonic has no user-avatar feature in Phase 1/2. |
Transcoding
Integrated into stream/download/hls above rather than a separate
endpoint (the classic API has none). internal/transcoding runs real
ffmpeg processes, bounded to 4 concurrent jobs, piping output directly to
the response (never buffering a full file), and is killed when the
client disconnects (context cancellation). Supported output formats:
mp3, opus, aac. maxBitRate, format, and timeOffset are all
honored.
Annotation
| Endpoint |
Version |
Implemented |
XML Tested |
JSON Tested |
Auth Tested |
Permission Tested |
Notes |
star |
1.4.0 |
✅ |
✅ |
— |
— |
— |
Song/album/artist, multiple IDs per call. |
unstar |
1.4.0 |
✅ |
✅ |
— |
— |
— |
|
setRating |
1.6.0 |
✅ |
✅ |
— |
— |
— |
Auto-detects song/album/artist from the given ID. |
scrobble |
1.5.0 |
✅ |
✅ |
— |
— |
— |
submission=true records playback history + a scrobble row; false only updates now-playing. |
Sharing
| Endpoint |
Version |
Implemented |
XML Tested |
JSON Tested |
Auth Tested |
Permission Tested |
Notes |
getShares |
1.6.0 |
✅ |
— |
✅ |
— |
✅ (share role) |
Own shares, or all shares for an admin. |
createShare |
1.6.0 |
✅ |
— |
✅ |
— |
✅ (share role) |
Songs and/or whole albums; real public URL. |
updateShare |
1.6.0 |
✅ |
— |
— |
— |
✅ (ownership) |
|
deleteShare |
1.6.0 |
✅ |
— |
— |
— |
✅ (ownership) |
|
A real public, unauthenticated share page/stream route also exists at
/share/{token} and /share/{token}/stream/{songID} — see
Web UI. Streaming through a share is still path-validated the
same way every other stream is; a share can never expose an arbitrary
filesystem path.
Podcasts
| Endpoint |
Version |
Implemented |
XML Tested |
JSON Tested |
Auth Tested |
Permission Tested |
Notes |
getPodcasts |
1.6.0 |
✅ |
— |
✅ |
— |
✅ (podcast role) |
|
getNewestPodcasts |
1.13.0 |
✅ |
— |
✅ |
— |
✅ (podcast role) |
|
refreshPodcasts |
1.9.0 |
✅ |
— |
— |
— |
✅ (admin) |
Real RSS fetch + parse for every channel, in the background. |
createPodcastChannel |
1.9.0 |
✅ |
— |
— |
— |
✅ (admin) |
Kicks off a real feed refresh. |
deletePodcastChannel |
1.9.0 |
✅ |
— |
— |
— |
✅ (admin) |
|
deletePodcastEpisode |
1.9.0 |
✅ |
— |
— |
— |
✅ (admin) |
|
downloadPodcastEpisode |
1.9.0 |
✅ |
— |
— |
— |
✅ (admin) |
Real bounded background download; the result becomes an ordinary streamable song. Interrupted downloads recover to "new" on restart (internal/podcasts.Manager.RecoverInterruptedDownloads, run at startup). |
internal/podcasts is a real subsystem: RSS/Atom parsing
(internal/podcasts/rss.go), episode metadata (GUID-keyed, stable
across refreshes), enclosure downloads bounded to 2 concurrent jobs, and
downloaded episodes registered as normal media_files/songs rows so
they stream through the exact same path as scanned music.
Jukebox
| Endpoint |
Version |
Implemented |
XML Tested |
JSON Tested |
Auth Tested |
Permission Tested |
Notes |
jukeboxControl |
1.2.0 (current form 1.7.0) |
✅ |
✅ |
✅ |
— |
✅ (jukebox role) |
Every action: get, status, set, start, stop, skip, add, clear, remove, shuffle, setGain. |
Backed by a real mpv process controlled over its JSON IPC socket
(internal/jukebox), tested against an actual mpv binary (playback,
pause/resume, add/remove/clear, gain, shuffle, index seeking all
verified in internal/jukebox/jukebox_test.go). If mpv isn't
installed, the jukebox is administratively unavailable and
jukeboxControl returns a clear error — never fake success.
Internet radio
| Endpoint |
Version |
Implemented |
XML Tested |
JSON Tested |
Auth Tested |
Permission Tested |
Notes |
getInternetRadioStations |
1.9.0 |
✅ |
— |
✅ |
— |
— |
|
createInternetRadioStation |
1.16.0 |
✅ |
✅ |
— |
— |
✅ (admin) |
|
updateInternetRadioStation |
1.16.0 |
✅ |
— |
— |
— |
✅ (admin) |
|
deleteInternetRadioStation |
1.16.0 |
✅ |
✅ |
— |
— |
✅ (admin) |
|
Also exposed in the web UI at /radio (viewing: any signed-in user;
managing: admin).
Chat
| Endpoint |
Version |
Implemented |
XML Tested |
JSON Tested |
Auth Tested |
Permission Tested |
Notes |
getChatMessages |
1.2.0 |
✅ |
— |
✅ |
— |
— |
Supports since. |
addChatMessage |
1.2.0 |
✅ |
— |
✅ |
— |
— |
|
User management
| Endpoint |
Version |
Implemented |
XML Tested |
JSON Tested |
Auth Tested |
Permission Tested |
Notes |
getUser |
1.3.0 |
✅ |
— |
✅ |
— |
✅ (self or admin) |
|
getUsers |
1.8.0 |
✅ |
— |
— |
— |
✅ (admin) |
|
createUser |
1.1.0 |
✅ |
✅ |
— |
— |
✅ (admin) |
Full role set + musicFolderId restriction. |
updateUser |
1.10.1 |
✅ |
✅ |
✅ |
— |
✅ (admin) |
Role params not present in the request are preserved, not reset to false (regression-tested). |
deleteUser |
1.3.0 |
✅ |
✅ |
— |
— |
✅ (admin; can't delete self) |
|
changePassword |
1.1.0 |
✅ |
✅ |
— |
— |
✅ (self or admin) |
Also refreshes the encrypted copy used for token auth. |
The full Subsonic role set (adminRole, settingsRole, streamRole,
jukeboxRole, downloadRole, uploadRole, playlistRole, coverArtRole,
commentRole, podcastRole, shareRole, videoConversionRole) is stored per
user and enforced on the server side by every endpoint above — never
only hidden in the UI. Per-user music folder restriction is also
enforced server-side (internal/subsonic/folders.go).
Bookmarks
| Endpoint |
Version |
Implemented |
XML Tested |
JSON Tested |
Auth Tested |
Permission Tested |
Notes |
getBookmarks |
1.9.0 |
✅ |
��” |
✅ |
— |
— |
Per-user. |
createBookmark |
1.9.0 |
✅ |
✅ |
— |
— |
— |
Position (ms) + comment. |
deleteBookmark |
1.9.0 |
✅ |
✅ |
— |
— |
— |
|
Play queue
| Endpoint |
Version |
Implemented |
XML Tested |
JSON Tested |
Auth Tested |
Permission Tested |
Notes |
getPlayQueue |
1.12.0 |
✅ |
— |
✅ |
— |
— |
Per-user, persisted. |
savePlayQueue |
1.12.0 |
✅ |
✅ |
✅ |
— |
— |
|
Scanner
| Endpoint |
Version |
Implemented |
XML Tested |
JSON Tested |
Auth Tested |
Permission Tested |
Notes |
getScanStatus |
1.15.0 |
✅ |
— |
✅ |
— |
— |
Real status/count from internal/scanner, not simulated. |
startScan |
1.15.0 |
✅ |
— |
✅ |
— |
✅ (admin) |
|
OpenSubsonic extensions
VibeSonic advertises these via getOpenSubsonicExtensions
(internal/subsonic/extensions.go); TestGetOpenSubsonicExtensionsNeverStubbed
in internal/subsonic/opensubsonic_core_test.go fails the build if an
extension is ever registered without its handler actually existing, so
this list can't drift ahead of what's real.
| Extension |
Versions |
Endpoint(s) |
Tested |
Notes |
apiKeyAuthentication |
1 |
tokenInfo, apiKey= param on every endpoint |
✅ |
Per-user named keys with plaintext shown once, SHA-256 hash stored, creation/last-used tracking, revocation (self or admin). apiKey conflicting with u returns error 43; an invalid/revoked key returns error 44. Managed at Settings → API Keys; admins see/revoke everyone's keys at /admin/api-keys. |
formPost |
1 |
(framework-wide) |
✅ |
application/x-www-form-urlencoded POST supported on every endpoint via net/http's ParseForm; a key present in both the POST body and the query string resolves to the body value (verified empirically and regression-tested). |
getPodcastEpisode |
1 |
getPodcastEpisode |
✅ |
Real lookup against the Phase 2 podcast subsystem; 404s honestly for an unknown episode. |
songLyrics |
1, 2 |
getLyricsBySongId |
✅ |
Structured lyrics (multiple entries per song, multiple languages) from embedded tags and sidecar .lrc/Enhanced LRC files, stored with UTF-8 byte offsets computed once at index time. enhanced=true additionally returns cueLine/cue word-level timing — but only for entries actually parsed from a source with real word timing (Enhanced LRC); plain and line-only-synced lyrics never get fabricated cues. |
indexBasedQueue |
1 |
getPlayQueueByIndex, savePlayQueueByIndex |
✅ |
Same play_queues storage as the classic queue, but exposes currentIndex directly instead of resolving it to a song ID — correct even when the queue repeats the same song at multiple positions (regression-tested). |
playbackReport |
1 |
reportPlayback |
✅ |
Persists user/media/position/state/client/timestamp (playback_reports table); state=stopped also clears now-playing and (unless ignoreScrobble) logs playback history. Never leaks one user's playback state to another. |
topSongsByArtistId |
1 |
getTopSongs (id param) |
✅ |
id (canonical internal artist ID) takes priority over the legacy artist name param when both are given. |
transcodeOffset |
1 |
stream (timeOffset param) |
✅ |
Forces a real transcode with an actual ffmpeg -ss seek — never decode-from-zero — verified by asserting the response isn't a range-servable direct passthrough. |
transcoding |
1 |
getTranscodeDecision, getTranscodeStream |
✅ |
Real capability matching: compares the source file's actual scanned format/channels against the client's directPlayProfiles; falls back to searching transcodingProfiles for a codec VibeSonic's ffmpeg pipeline can actually encode (mp3/opus/aac) that also satisfies any codecProfile limitations the client marked required. canDirectPlay/canTranscode are never set true without an actual match. getTranscodeStream takes the opaque transcodeParams token verbatim and streams real transcoded audio with real seek support. |
sonicSimilarity |
1 |
getSonicSimilarTracks, findSonicPath |
✅ |
Backed by internal/audiofeatures (real ffmpeg PCM decode + a hand-written FFT for tempo/loudness/spectral-shape/chroma, computed by the scanner as part of each scan) and internal/similarity (an in-process, z-score-normalized nearest-neighbor index — no external vector database). A song that hasn't been analyzed yet simply doesn't appear in results; findSonicPath walks real feature-space geometry to a real intermediate song at each hop, never an invented one. |
OpenSubsonic response/model additions
subsonic-response: type, serverVersion, openSubsonic — always
populated (internal/subsonic/model.go).
error.helpUrl — set on API-key-related errors (43, 44), pointing at
the Settings API Keys page.
Child/other entity fields VibeSonic already populated correctly
before Phase 3 (replayGain*, musicBrainzId, disc/track totals,
etc.) are unchanged; nothing new was added here beyond what's listed
in the extension table above (structured lyrics, sonic-match
entries, transcode-decision/stream-details, index-based play queue).
Optional external metadata (MusicBrainz / Cover Art Archive)
Not an OpenSubsonic extension itself, but part of Phase 3's metadata
work: getArtistInfo(2)/getAlbumInfo(2) fetch a real MusicBrainz
artist disambiguation or Cover Art Archive front-cover image when
VIBESONIC_ENABLE_EXTERNAL_METADATA=true and the entity already
carries a locally-tagged MusicBrainz ID, caching results in
external_metadata_cache for 30 days. VibeSonic works completely
without Internet access: the flag defaults off, and any lookup
failure/timeout silently falls back to local-only fields rather than
erroring the request. See internal/metadataenrich and
internal/subsonic/enrich.go.
Authentication
Both original Subsonic authentication modes are implemented in
internal/subsonic/auth.go:
- Token/salt (
u, t, s): t = md5(password + salt). Verified
against a separate, AES-256-GCM–encrypted copy of the password
(internal/auth/subsonic_secret.go) kept in sync with every password
change — never against the Argon2id hash (impossible, by design) and
never by storing the password in the clear.
- Password (
p): plain or enc:<hex>, verified against the
Argon2id hash used everywhere else in VibeSonic.
An account created purely through external (trusted-proxy) auth has no
local password and therefore cannot authenticate to /rest/* until an
administrator or the user sets one — /rest/* is completely independent
of the browser session/CSRF/external-auth flow (see
docs/external-auth.md).
Common parameters
u, p, t, s, v, c, f (xml/json/jsonp), and callback are
all handled centrally in internal/subsonic/router.go and
internal/subsonic/auth.go — no handler parses or validates them
itself. v is checked against ServerVersion (currently 1.16.1): a
client claiming a newer protocol version gets error 20; older clients
are accepted, since the protocol is additive.
Error codes
All defined in internal/subsonic/errors.go: 0 (generic), 10 (missing
parameter), 20 (client must upgrade), 30 (server must upgrade, reserved
— VibeSonic never needs to require a newer client), 40 (wrong
credentials), 41 (token auth unsupported for this account), 42 (auth
mechanism not supported), 43 (conflicting auth mechanisms — e.g. both
apiKey and u given), 44 (invalid/revoked API key), 50 (unauthorized
for the operation), 60 (license/trial — never returned), 70 (not
found).
Web UI
Newly exposed through the Bootstrap UI in Phase 2: Podcasts (/podcasts,
admin at /admin/podcasts), Internet Radio (/radio), Shares
(/shares, plus the public /share/{token} page), Bookmarks
(/bookmarks), per-user Subsonic permission editing
(/admin/users/{id}/permissions), and API key management (Settings →
API Keys for any user; /admin/api-keys for admins to view/revoke
everyone's). The admin Jukebox page
(/admin/jukebox) shows live status and emergency stop/clear; building
a jukebox playlist is done from a Subsonic client via jukeboxControl,
which is the actual protocol surface for that feature.