Phase 2: exhaustive Subsonic 1.16.1 compatibility matrix, README/config/Docker updates

This commit is contained in:
2026-08-23 00:38:09 -05:00
parent f1fdd8d2c2
commit 6ac50994b8
6 changed files with 305 additions and 112 deletions
+258 -102
View File
@@ -1,125 +1,281 @@
# Subsonic / OpenSubsonic compatibility
# Subsonic API compatibility
VibeSonic aims to eventually implement the Subsonic API (`/rest/*`) and
OpenSubsonic extensions. **Phase 1 implements none of this** — it only
builds the foundation (database schema, stable IDs, library indexing,
streaming, cover art) that the protocol layer will sit on top of in a
later phase.
VibeSonic implements the **complete original Subsonic REST API through
version 1.16.1** as of Phase 2. Every endpoint below is backed by real
data from VibeSonic's own database/scanner/streaming/transcoding/jukebox
subsystems — none of them are stub handlers that return an empty
successful response.
This matrix will be filled in as `internal/subsonic` and
`internal/opensubsonic` are implemented. For now, every endpoint is
"Not yet implemented."
OpenSubsonic extensions are **not yet implemented** (planned for Phase
3), except where the underlying architecture already needed to make a
choice (e.g. opaque string IDs almost everywhere) that happens to also
suit OpenSubsonic later.
## 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 | Status |
|---|---|
| `ping` | Not yet implemented |
| `getLicense` | Not yet implemented |
| 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. |
## Browsing (filesystem-style)
## Music folders
| Endpoint | Status | Notes |
|---|---|---|
| `getMusicFolders` | Not yet implemented | `music_folders` table and `internal/database` queries exist |
| `getIndexes` | Not yet implemented | `internal/library.ListDirectory` provides the foundation |
| `getMusicDirectory` | Not yet implemented | same |
| 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 (ID3/tag-style)
## Browsing
| Endpoint | Status | Notes |
|---|---|---|
| `getArtists` | Not yet implemented | `database.ListArtists` exists |
| `getArtist` | Not yet implemented | `database.ListAlbumsByArtist` exists |
| `getAlbum` | Not yet implemented | `database.ListSongsByAlbum` exists |
| `getSong` | Not yet implemented | `database.GetSong` exists |
| `getGenres` | Not yet implemented | `database.ListGenres` exists |
| `getAlbumList` / `getAlbumList2` | Not yet implemented | |
| `getRandomSongs` | Not yet implemented | |
| `getSongsByGenre` | Not yet implemented | |
| 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 | ⚠️ | — | — | — | — | 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. |
| `getArtistInfo2` | 1.11.0 | ⚠️ | — | — | — | — | Same as above, ID3 form. |
| `getAlbumInfo` | 1.14.0 | ⚠️ | — | — | — | — | Same reasoning as `getArtistInfo`. |
| `getAlbumInfo2` | 1.14.0 | ⚠️ | — | — | — | — | Same. |
| `getSimilarSongs` | 1.11.0 | ✅ | — | — | — | — | Real, local genre-overlap heuristic (song/album/artist seed), not fabricated data. |
| `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 | Status |
|---|---|
| `search3` | Not yet implemented |
## Streaming / media retrieval
| Endpoint | Status | Notes |
|---|---|---|
| `stream` | Not yet implemented | `internal/streaming` provides range-request-aware serving; the web UI's `/stream/{id}` route is the non-Subsonic equivalent used today |
| `download` | Not yet implemented | |
| `getCoverArt` | Not yet implemented | `internal/coverart` provides discovery + resize cache; the web UI's `/cover-art/{id}` route is the non-Subsonic equivalent used today |
| `getLyrics` / `getLyricsBySongId` | Not yet implemented | `lyrics`/`lyrics_lines`/`lyrics_cues` tables exist |
| `getAvatar` | Not yet implemented | |
| 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 | Status | Notes |
|---|---|---|
| `getPlaylists` | Not yet implemented | `database.ListPlaylistsVisibleToUser` exists; web UI has a playlists page |
| `getPlaylist` | Not yet implemented | |
| `createPlaylist` | Not yet implemented | |
| `updatePlaylist` | Not yet implemented | |
| `deletePlaylist` | Not yet implemented | |
| 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) | |
## Favorites / ratings / activity
## Media retrieval
| Endpoint | Status | Notes |
|---|---|---|
| `star` / `unstar` | Not yet implemented | `database.Star`/`Unstar` exist; web UI has favorite buttons |
| `setRating` | Not yet implemented | `database.SetRating` exists |
| `scrobble` | Not yet implemented | `scrobbles` table exists |
| `getStarred` / `getStarred2` | Not yet implemented | |
| `getNowPlaying` | Not yet implemented | `now_playing` table + `database.ListNowPlaying` exist |
## Bookmarks / play queue
| Endpoint | Status | Notes |
|---|---|---|
| `getBookmarks` / `createBookmark` / `deleteBookmark` | Not yet implemented | `database` functions exist |
| `getPlayQueue` / `savePlayQueue` | Not yet implemented | `database` functions exist |
## Sharing
| Endpoint | Status | Notes |
|---|---|---|
| `getShares` / `createShare` / `updateShare` / `deleteShare` | Not yet implemented | `shares`/`share_items` tables exist |
## Podcasts
| Endpoint | Status | Notes |
|---|---|---|
| `getPodcasts` | Not yet implemented | `podcast_channels`/`podcast_episodes` tables exist |
| `createPodcastChannel` / `deletePodcastChannel` | Not yet implemented | |
| `downloadPodcastEpisode` / `deletePodcastEpisode` | Not yet implemented | |
## Internet radio
| Endpoint | Status | Notes |
|---|---|---|
| `getInternetRadioStations` | Not yet implemented | `internet_radio_stations` table exists |
| `createInternetRadioStation` / `updateInternetRadioStation` / `deleteInternetRadioStation` | Not yet implemented | |
## Users / chat
| Endpoint | Status | Notes |
|---|---|---|
| `getUser` / `getUsers` | Not yet implemented | native web admin has a users page |
| `createUser` / `updateUser` / `deleteUser` | Not yet implemented | |
| `addChatMessage` / `getChatMessages` | Not yet implemented | `chat_messages` table exists |
| 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 | ✅ | ✅ | — | — | — | Real embedded lyrics extracted at scan time; empty (not fabricated) if none found. |
| `getAvatar` | 1.8.0 | ⚠️ | — | — | — | — | Honestly 404s — VibeSonic has no user-avatar feature in Phase 1/2. |
## Transcoding
| Endpoint | Status | Notes |
|---|---|---|
| Transcoding profiles (`internal/transcoding`) | Not yet implemented | `transcode_profiles` table exists; Phase 1 only supports direct (non-transcoded) streaming |
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](#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) | |
---
## Authentication
Phase 1's `/rest/*` authentication (token-based `u`/`t`/`s` or API key)
is not yet implemented. When it is, it will authenticate independently
of the browser session/CSRF/external-auth flow used by the web UI, per
the project's design constraint that external auth must never interfere
with native Subsonic client 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`](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), 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`), and per-user Subsonic permission editing
(`/admin/users/{id}/permissions`). 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.