Update docs/subsonic-compatibility.md with the OpenSubsonic extension matrix

Documents every currently-registered OpenSubsonic extension
(apiKeyAuthentication, formPost, getPodcastEpisode, songLyrics,
indexBasedQueue, playbackReport, topSongsByArtistId, transcodeOffset,
transcoding, sonicSimilarity), what backs each one, and what it's
tested against — cross-referencing the extensions-registry regression
guard that fails the build if an advertised extension's handler ever
goes missing. Also updates the getArtistInfo/getAlbumInfo/
getSimilarSongs rows and error-code list for Phase 3 changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014r98AtcBbevpscWLsfwVvv
This commit is contained in:
2026-08-23 08:36:48 -05:00
co-authored by Claude Sonnet 5
parent a921f9e2bc
commit 238cf0d4a0
+69 -20
View File
@@ -1,15 +1,14 @@
# Subsonic API compatibility # Subsonic API compatibility
VibeSonic implements the **complete original Subsonic REST API through VibeSonic implements the **complete original Subsonic REST API through
version 1.16.1** as of Phase 2. Every endpoint below is backed by real version 1.16.1** (Phase 2) plus a growing set of **OpenSubsonic
data from VibeSonic's own database/scanner/streaming/transcoding/jukebox extensions** (Phase 3). Every endpoint below is backed by real data
subsystems — none of them are stub handlers that return an empty from VibeSonic's own database/scanner/streaming/transcoding/jukebox/
successful response. audio-analysis subsystems — none of them are stub handlers that return
an empty successful response. `getOpenSubsonicExtensions` only ever
OpenSubsonic extensions are **not yet implemented** (planned for Phase advertises an extension once its handler and tests exist and pass; see
3), except where the underlying architecture already needed to make a [OpenSubsonic extensions](#opensubsonic-extensions) below for the full
choice (e.g. opaque string IDs almost everywhere) that happens to also list and what backs each one.
suit OpenSubsonic later.
## How to read this table ## How to read this table
@@ -56,11 +55,11 @@ suit OpenSubsonic later.
| `getSong` | 1.8.0 | ✅ | — | ✅ | ✅ | ✅ (folder restriction) | | | `getSong` | 1.8.0 | ✅ | — | ✅ | ✅ | ✅ (folder restriction) | |
| `getVideos` | 1.8.0 | ✅ | — | ✅ | — | — | Real: lists media files the scanner tagged `kind=video`. | | `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. | | `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 | ⚠️ | — | — | — | — | Honestly returns empty biography/images/similar-artists — VibeSonic has no external metadata provider in Phase 2. Not a fake success: it's an accurate "we don't have this" response. | | `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](#opensubsonic-extensions). |
| `getArtistInfo2` | 1.11.0 | ⚠️ | — | — | — | — | Same as above, ID3 form. | | `getArtistInfo2` | 1.11.0 | ✅ | — | ✅ | — | — | Same as above, ID3 form. |
| `getAlbumInfo` | 1.14.0 | ⚠️ | — | — | — | — | Same reasoning as `getArtistInfo`. | | `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. | | `getAlbumInfo2` | 1.14.0 | ✅ | — | ✅ | — | — | Same. |
| `getSimilarSongs` | 1.11.0 | ✅ | — | — | — | — | Real, local genre-overlap heuristic (song/album/artist seed), not fabricated data. | | `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. | | `getSimilarSongs2` | 1.11.0 | ✅ | — | — | — | — | ID3 (artist-seeded) form. |
| `getTopSongs` | 1.13.0 | ✅ | — | ✅ | — | — | Ranked by real aggregate play count across all users. | | `getTopSongs` | 1.13.0 | ✅ | — | ✅ | — | — | Ranked by real aggregate play count across all users. |
@@ -103,7 +102,7 @@ suit OpenSubsonic later.
| `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. | | `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. | | `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. | | `getCoverArt` | 1.0.0 | ✅ | n/a | n/a | — | ✅ (coverArt role) | Embedded or folder art, resized and cached on disk. |
| `getLyrics` | 1.2.0 | ✅ | ✅ | — | — | — | Real embedded lyrics extracted at scan time; empty (not fabricated) if none found. | | `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. | | `getAvatar` | 1.8.0 | ⚠️ | — | — | — | — | Honestly 404s — VibeSonic has no user-avatar feature in Phase 1/2. |
## Transcoding ## Transcoding
@@ -232,6 +231,52 @@ enforced server-side (`internal/subsonic/folders.go`).
--- ---
## 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 ## Authentication
Both original Subsonic authentication modes are implemented in Both original Subsonic authentication modes are implemented in
@@ -265,17 +310,21 @@ are accepted, since the protocol is additive.
All defined in `internal/subsonic/errors.go`: 0 (generic), 10 (missing All defined in `internal/subsonic/errors.go`: 0 (generic), 10 (missing
parameter), 20 (client must upgrade), 30 (server must upgrade, reserved parameter), 20 (client must upgrade), 30 (server must upgrade, reserved
— VibeSonic never needs to require a newer client), 40 (wrong — VibeSonic never needs to require a newer client), 40 (wrong
credentials), 41 (token auth unsupported for this account), 50 credentials), 41 (token auth unsupported for this account), 42 (auth
(unauthorized for the operation), 60 (license/trial — never returned), mechanism not supported), 43 (conflicting auth mechanisms — e.g. both
70 (not found). `apiKey` and `u` given), 44 (invalid/revoked API key), 50 (unauthorized
for the operation), 60 (license/trial — never returned), 70 (not
found).
## Web UI ## Web UI
Newly exposed through the Bootstrap UI in Phase 2: Podcasts (`/podcasts`, Newly exposed through the Bootstrap UI in Phase 2: Podcasts (`/podcasts`,
admin at `/admin/podcasts`), Internet Radio (`/radio`), Shares admin at `/admin/podcasts`), Internet Radio (`/radio`), Shares
(`/shares`, plus the public `/share/{token}` page), Bookmarks (`/shares`, plus the public `/share/{token}` page), Bookmarks
(`/bookmarks`), and per-user Subsonic permission editing (`/bookmarks`), per-user Subsonic permission editing
(`/admin/users/{id}/permissions`). The admin Jukebox page (`/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 (`/admin/jukebox`) shows live status and emergency stop/clear; building
a jukebox playlist is done from a Subsonic client via `jukeboxControl`, a jukebox playlist is done from a Subsonic client via `jukeboxControl`,
which is the actual protocol surface for that feature. which is the actual protocol surface for that feature.