WaveFlow exposes a tested Subsonic 1.16.1 façade for existing music clients. This guide covers server configuration, authentication, wire formats and the implemented method surface. The frozen compatibility decisions live in RFC-002, and real-client results are in subsonic-compatibility.md.
Enter the server origin as the server URL:
https://music.example.com
Most clients append /rest/<method> themselves. Do not enter /api/v2, and
only append /rest when a particular client explicitly asks for an API path.
Use the WaveFlow username plus its dedicated Subsonic password. The web
password is deliberately not accepted by /rest. An administrator creates or
rotates the credential with either:
$env:WAVEFLOW_SUBSONIC_PASSWORD = "a-different-app-password"
cargo run -- credential set --actor admin --username listeneror PUT /api/v2/admin/users/{username}/subsonic-credential as documented in
the native API guide. Both paths print or return an API key
exactly once.
WaveFlow has been validated with Symfonium, Feishin, DSub, Substreamer and Juliet. See the compatibility matrix for exact versions and exercised features.
Both route forms are accepted:
/rest/ping
/rest/ping.view
Requests may use GET query parameters or
application/x-www-form-urlencoded POST bodies. XML is the default; add
f=json for JSON. Include the normal client identification parameters for
maximum third-party compatibility:
v=1.16.1
c=my-client
Example JSON ping using form POST:
curl -X POST https://music.example.com/rest/ping.view \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "apiKey=SUBSONIC_API_KEY" \
--data-urlencode "v=1.16.1" \
--data-urlencode "c=my-client" \
--data-urlencode "f=json"Successful JSON is wrapped in subsonic-response:
{
"subsonic-response": {
"status": "ok",
"version": "1.16.1",
"type": "waveflow",
"serverVersion": "...",
"openSubsonic": true
}
}XML uses the standard Subsonic response root and namespace. JSON collection fields remain arrays even when empty or containing a single item.
WaveFlow accepts three authentication modes.
apiKey=SUBSONIC_API_KEY
The key identifies the account, so u is not required. This is the preferred
mode for a client that implements the OpenSubsonic apiKeyAuthentication
extension.
u=listener
s=RANDOM_SALT
t=md5(subsonic_password + salt)
The MD5 construction is required by the legacy protocol and protects the dedicated password from being sent directly. Use a fresh unpredictable salt per authentication attempt.
u=listener&p=a-different-app-password
WaveFlow also accepts p=enc:<hexadecimal UTF-8 password bytes>. This is only
wire compatibility, not encryption. Use HTTPS for every authentication mode.
Authentication failures use Subsonic error code 40 without distinguishing an
unknown, disabled or incorrectly authenticated user. Repeated failures are rate
limited and reported with that same code, so a throttled client sees exactly
what a wrong password produces. WaveFlow request tracing records only the path, never credentials or
query parameters; clients should still prefer form POST so their own URL logs
do not retain secrets.
The examples use apiKey for brevity. Replace it with u/t/s or u/p when
required by the client.
List music folders:
curl -X POST https://music.example.com/rest/getMusicFolders.view \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "apiKey=SUBSONIC_API_KEY" \
--data-urlencode "v=1.16.1" \
--data-urlencode "c=my-client" \
--data-urlencode "f=json"Search with independent page controls:
curl -X POST https://music.example.com/rest/search3.view \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "apiKey=SUBSONIC_API_KEY" \
--data-urlencode "v=1.16.1" \
--data-urlencode "c=my-client" \
--data-urlencode "f=json" \
--data-urlencode "query=bjork" \
--data-urlencode "artistCount=20" \
--data-urlencode "artistOffset=0" \
--data-urlencode "albumCount=20" \
--data-urlencode "albumOffset=0" \
--data-urlencode "songCount=100" \
--data-urlencode "songOffset=0"The literal query "" is match-all for full catalogue pagination. Counts are
capped at 500. Repeated musicFolderId parameters select the union of visible
libraries; inaccessible IDs never expose foreign catalogue data.
Create a playlist while preserving repeated track order:
curl -X POST https://music.example.com/rest/createPlaylist.view \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "apiKey=SUBSONIC_API_KEY" \
--data-urlencode "v=1.16.1" \
--data-urlencode "c=my-client" \
--data-urlencode "f=json" \
--data-urlencode "name=Road trip" \
--data-urlencode "songId=FIRST_TRACK_UUID" \
--data-urlencode "songId=SECOND_TRACK_UUID"Read synchronized lyrics by stable track UUID:
curl -X POST https://music.example.com/rest/getLyricsBySongId.view \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "apiKey=SUBSONIC_API_KEY" \
--data-urlencode "v=1.16.1" \
--data-urlencode "c=my-client" \
--data-urlencode "f=json" \
--data-urlencode "id=TRACK_UUID"WaveFlow returns embedded text and UTF-8 .lrc/.txt sidecars. LRC timestamps
are milliseconds. The legacy getLyrics exact artist/title lookup is also
implemented.
Stream an original with a byte range:
curl "https://music.example.com/rest/stream.view?apiKey=SUBSONIC_API_KEY&v=1.16.1&c=my-client&id=TRACK_UUID" \
-H "Range: bytes=0-1048575" \
--output part.binRequest a transcode and temporal seek:
curl "https://music.example.com/rest/stream.view?apiKey=SUBSONIC_API_KEY&v=1.16.1&c=my-client&id=TRACK_UUID&format=opus&maxBitRate=96&timeOffset=30" \
--output track.oggValid explicit formats are raw, mp3 and opus; ogg is accepted as an
alias for Opus. timeOffset is in seconds.
Without an explicit format, maxBitRate is a ceiling: WaveFlow keeps the
original when its known bitrate fits and otherwise transcodes to MP3. Original
and completed cached responses support Range; live transcodes are chunked.
Only the following methods are dispatched. Unknown methods answer HTTP 200
with Subsonic error code 0, like every other protocol failure.
| Group | Methods | Notes |
|---|---|---|
| System | ping, getLicense, getOpenSubsonicExtensions, tokenInfo |
Extensions are listed below. tokenInfo returns the username the presented credential resolves to. |
| Catalogue roots | getMusicFolders, getIndexes, getArtists, getArtist, getAlbum, getSong, getGenres, getMusicDirectory |
IDs and musicFolderId values are UUIDs. |
| Artist and album information | getArtistInfo, getArtistInfo2, getAlbumInfo, getAlbumInfo2 |
Validate tenant access and return the standard empty container; enrichment is not implemented. |
| Album discovery | getAlbumList, getAlbumList2, getRandomSongs, getSongsByGenre |
Both legacy and ID3 album-list containers are supported. |
| Search | search3, search2 |
Independent artist, album and song pagination. search2 is the same payload in the searchResult2 container. |
| Playlists | getPlaylists, getPlaylist, createPlaylist, updatePlaylist, deletePlaylist |
Shared with native/web user data. |
| Media | stream, download, getCoverArt |
Tenant-authorized streaming and artwork. |
| Lyrics | getLyrics, getLyricsBySongId |
OpenSubsonic songLyrics v1 plus legacy lookup. |
| Favorites and ratings | star, unstar, getStarred2, getStarred, setRating |
Track, album and artist IDs are tenant scoped. getStarred is the same payload in the starred container. |
| Activity and queue | scrobble, getNowPlaying, getPlayQueue, savePlayQueue |
Queue order and duplicate tracks are preserved. |
| Shares | getShares, createShare, updateShare, deleteShare |
Creation returns the public URL; later reads omit the bearer token. |
| Users | getUser, getUsers, createUser, updateUser, deleteUser, changePassword |
Administrative methods require an admin account. changePassword changes only the Subsonic credential. |
| Library maintenance | startScan, getScanStatus |
Rescans every library the account owns or manages and reports progress. |
| Bookmarks | getBookmarks, createBookmark, deleteBookmark |
One position per account and track; setting it again moves it. |
| Compatibility | getTopSongs, getSimilarSongs, getSimilarSongs2, getInternetRadioStations |
Return the standard empty container. WaveFlow computes no recommendations and hosts no radio. |
| Missing data | getAvatar |
No avatars are stored, so it answers error code 70. |
getGenres folds spelling variants onto one row: case, punctuation and
spacing are normalised, so "Hip-Hop", "hip hop" and "HIP HOP" are one
genre with one count. getSongsByGenre, getRandomSongs?genre= and
getAlbumList2?type=byGenre match the same way, so any spelling of a genre
returns everything in it. Until this release the two song methods compared
the raw display string with an ASCII case fold, so asking for a genre
getGenres had just listed could return a fraction of its tracks, or none.
createBookmark takes id (a track) and position in milliseconds, plus an
optional comment. There is one bookmark per account and track — it answers
"where did I stop in this file" — so calling it again moves the existing one
and omitting comment clears it. deleteBookmark takes id and succeeds
whether or not a bookmark was there.
getBookmarks returns each one with position, username, created,
changed, an optional comment and an entry holding the full media item,
which additionally carries bookmarkPosition. A bookmark on a track that has
become unavailable, or in a library the account has lost, stops being listed
rather than being returned pointing at nothing.
Bookmarks are user data like favorites and ratings, so they reach
/api/v2/sync/changes under the bookmark entity type and the bootstrap
/api/v2/sync/snapshot.
startScan takes no library parameter — it rescans every library the
authenticated account may scan, and answers with the same scanStatus
element getScanStatus returns, so a client that only calls startScan
still learns the state:
<scanStatus scanning="true" count="1234"/>count is the number of available tracks this account can reach, not
what the instance holds. scanning is true while any of those libraries has
a queued or running job. Scans are asynchronous: a scanning="false"
immediately after startScan means the work finished, not that it never
started. The equivalents are POST /api/v2/libraries/{library_id}/scans,
which scans one library, and GET /api/v2/scans/{scan_id} with its
server-sent progress stream.
Being a member of a library is not enough to rescan it. A scan walks the
owner's files and takes the instance's write lock, so it is reserved to the
owner and manager roles; a listener reads the catalogue only. Libraries
the account may only listen to are skipped, not refused: startScan names no
library, so an account whose every library is read-only queues nothing and
still answers ok, and count keeps reporting what that account can reach.
The per-library native route, which does name one, answers 404 — the same
answer a library that does not exist gets.
Songs carry mediaType, isVideo, samplingRate, channelCount, bitDepth,
playCount, displayArtist, artists[], albumArtists[],
displayAlbumArtist, contributors[], displayComposer, genres[],
musicBrainzId, bpm, sortName, comment, isrc[], moods[],
explicitStatus and replayGain; albums add isCompilation, playCount,
displayArtist, sortName, artists[] and genres[]; artists add sortName
and roles[]. Both songs
and albums carry played when they have been played.
albumArtists[] and displayAlbumArtist are the album's credit, not the
track's: a guest appearance names the guest in artists[], while the album still
belongs under the album artist. An album's artists[] is the album's own credit
— the artists it is credited to — while its genres[] are the union of its
available tracks', folded on the canonical name, so an album spelling "Hip-Hop"
on some tracks and "Hip Hop" on others reports one genre.
contributors[] names everyone else the file credits: composer, lyricist,
conductor, arranger, producer, director, engineer, mixer, remixer, DJ mixer and
performer. Each entry is the role, the instrument when a performer names one
(subRole), and an artist reference. displayComposer is the composers joined
with •. roles[] on an artist is the capacities it is credited in anywhere in
the catalogue. All three are ordered deterministically, so two responses for one
record are byte-identical.
An artist credited in no album — a composer, say — is reachable by identifier
and by search, but getArtists and getIndexes list only the artists an album
is credited to.
What WaveFlow does not implement is absent rather than empty, which under the
presence rule is what says so: moods[], explicitStatus,
originalReleaseDate, releaseDate, releaseTypes[], recordLabels[] and
discTitles[] on an album. Those need album columns the schema does not have.
sortName on an album and an artist is no longer among them, and neither are
contributors[], displayComposer or roles[]: every credit a file names is
stored under the role it names it under, so a composer, a producer or a
performer is an artist row like any other.
These follow the OpenSubsonic presence rule: a supported field is present even
when WaveFlow has no value for it, so an untagged track answers samplingRate=0,
displayArtist="" and an empty genres array. Do not read an absent field as
an empty one — absence means the field is not implemented at all. explicitStatus is normalised to explicit or clean; a tag that says "no
advisory" is not a claim that the work is clean, so it sends the empty value.
musicBrainzId means a different entity on each item, which is why it is not
the same value everywhere. On a song it is the MusicBrainz recording
identifier — the performance. On an album it is the release, and on an
artist the artist; both are also under the presence rule, so an untagged
album answers musicBrainzId="". The release and artist identifiers are never
sent at track level, where they would name a different entity.
An album's identifier is derived, not read from one file. Tracks of one album routinely disagree — a library assembled over years holds files tagged against different releases of the same record — so the album takes the identifier most of its available tracks agree on, recomputed at the end of every scan. Ties fall to the earliest disc and track, so two scans of unchanged files answer the same thing. An artist takes the identifier from the tracks it is the first credit of, because the tag is one value on a file that may credit several artists.
getAlbumInfo and getAlbumInfo2 carry that release identifier as their
musicBrainzId element. They remain otherwise empty: WaveFlow queries no remote
source, so there are no notes and no biography images. Being a classic Subsonic
response rather than an OpenSubsonic one, the element is omitted when the album
has no identifier instead of being sent empty.
Browsing entries are the exception. getMusicDirectory renders artists and
albums as child elements, and on a child the specification defines
musicBrainzId as the recording id; a folder standing for an artist or a
release has no recording, so the field is dropped there rather than carrying a
different identifier under that name. Read album and artist identifiers from
getAlbum, getArtist, getAlbumList2, getArtists and search3.
replayGain is an object whose members are omitted when unknown, on the
specification's instruction; the object itself is always present, so an untagged
track answers an empty one. isrc is an array, repeated <isrc> elements in
XML. Both are filled by a scan: a library indexed before this release reports
these fields supported and empty until it is rescanned, which is exactly what the
presence rule means and needs no client change.
played is the one exception: it is sent only once the item has been played,
because its empty value would be an empty string rather than a timestamp.
playCount is always present and signals the same support.
artists[] is every credited artist in tag order, each with id and name;
artist and artistId remain the display string and the primary credit.
genres[] is the split, deduplicated genre list ordered by name, while genre
remains the raw tag string. In XML both are repeated child elements
(<artists id="..." name="..."/>, <genres name="..."/>); in JSON both are
arrays, [] when empty.
Album-list types are random, newest, highest, frequent, recent,
starred, alphabeticalByName, alphabeticalByArtist, byYear and byGenre.
Non-random results use stable title/UUID tie-breaking. byGenre matches the
canonical genre name, folding case, punctuation and spacing, so Hip-Hop and
hip hop select the same albums; it requires genre and answers error code 10
without it, rather than returning the catalogue unfiltered. A reversed
fromYear/toYear pair returns the range in descending order. size=0 answers with an empty container. All ten
types are ordered and paged in SQL; the same vocabulary is available natively as
GET /api/v2/albums?sort=.
Repeated parameters such as songId, songIdToAdd, songIndexToRemove,
musicFolderId, scrobble id/time, queue id and share IDs retain wire
order. Playlist removals are applied from the highest index downward before
additions.
getOpenSubsonicExtensions advertises only tested behavior:
| Extension | Version | Behavior |
|---|---|---|
formPost |
1 | Form POST requests are accepted. |
apiKeyAuthentication |
1 | apiKey can replace u/p or u/t/s; tokenInfo resolves it to a username. |
transcodeOffset |
1 | timeOffset seeks transcoded playback. |
songLyrics |
1 | Plain and line-synchronized lyrics by song ID. |
Word-level lyrics, translations and other songLyrics v2 fields are not
declared. Do not infer support for an extension that is absent from this list.
Protocol errors keep the normal Subsonic envelope and include numeric code
and message. Important codes include:
| Code | Meaning |
|---|---|
| 0 | Generic error or method not implemented. |
| 10 | Required parameter missing. |
| 40 | Authentication failed. |
| 50 | User lacks the required role. |
| 70 | Requested resource is missing or inaccessible. |
Every protocol answer is HTTP 200, success and failure alike: the outcome
lives in the envelope status and, when it failed, in error/code. Do not
branch on the HTTP status — it carries no protocol meaning here. Byte-range
responses on stream and download are the exception and still answer 206
and 416, because those are transport facts rather than protocol outcomes.
Resource lookups are tenant-scoped; code 70 does not reveal whether a foreign
UUID exists. Authentication throttling is reported as code 40, identical to a
wrong password.
Set an exact comma-separated origin allow-list, for example:
WAVEFLOW_ALLOWED_ORIGINS=http://127.0.0.1:9180,https://player.example.comAllowed origins may use GET, form POST and OPTIONS and can read the range headers needed for web audio playback. Wildcard origins are intentionally not supported with credential-bearing requests.
createShare returns an absolute URL when WAVEFLOW_PUBLIC_URL is configured,
otherwise a relative /share/{token} URL. Preserve that creation response:
getShares and synchronization snapshots omit the plaintext bearer token. A
native /api/v2 caller that supplied an operation ID can recover the same URL
by replaying the identical creation operation, but the Subsonic method has no
operation-ID parameter. The public metadata payload contains token-scoped
stream URLs and cannot stream tracks outside the share.