282 lines
17 KiB
Markdown
282 lines
17 KiB
Markdown
# Subsonic API compatibility
|
||
|
||
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.
|
||
|
||
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 | 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 | ⚠️ | — | — | — | — | 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 | 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 | ✅ | ✅ | — | — | — | 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
|
||
|
||
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
|
||
|
||
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.
|