Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
16072e3
fix(media): answer a browser's opening range on a cold transcode
InstaZDLL Aug 19, 2026
dd1336e
docs(subsonic): record the Symfonium re-run against the current contract
InstaZDLL Aug 19, 2026
66af383
fix(subsonic): createPlaylist replaces the track list it is given
InstaZDLL Aug 19, 2026
0c9ff22
docs(subsonic): record the Feishin re-run and what it found
InstaZDLL Aug 19, 2026
d48c726
docs(subsonic): drop substreamer from the replayed set, juliet takes …
InstaZDLL Aug 19, 2026
8f3fa12
fix(catalog): an album hangs off its first credited artist
InstaZDLL Aug 19, 2026
13c1f57
docs(subsonic): record the dsub re-run and correct two claims of the …
InstaZDLL Aug 19, 2026
d34730a
fix(media): a cold transcode answers the iOS two-byte probe too
InstaZDLL Aug 19, 2026
a24081c
docs(subsonic): record the juliet re-run, the last of the five rows
InstaZDLL Aug 19, 2026
c81b129
docs(subsonic): the fixed cold-transcode path was re-played by the cl…
InstaZDLL Aug 19, 2026
7f3afe8
fix(catalog): recompute the album identity when repairing its artist
InstaZDLL Aug 19, 2026
0aff1de
fix(sync): keep a playlist replay valid across this upgrade
InstaZDLL Aug 19, 2026
e615180
fix(media): refuse a malformed range, honour a range the cache can serve
InstaZDLL Aug 19, 2026
fb4b4f0
docs(subsonic): the http 200 deviation is a result, not an expectation
InstaZDLL Aug 19, 2026
dafe3c3
fix(catalog): an album identity names the whole credit, not just its …
InstaZDLL Aug 19, 2026
524cd4b
docs(subsonic): attribute the play-queue coverage to the run that has it
InstaZDLL Aug 19, 2026
6151cab
fix(catalog): the album identity keeps its credit boundaries
InstaZDLL Aug 19, 2026
725576a
test(catalog): drive the credit-boundary case from the album credit a…
InstaZDLL Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 39 additions & 24 deletions docs/subsonic-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,42 @@ Automated protocol coverage is enforced by `tests/v2_foundations.rs` for XML, JS

| Client | Version | Login | Browse/search | Native/transcode | Playlists/user data | Status |
|---|---:|---:|---:|---:|---:|---|
| Symfonium | 14.1.0 | pass | pass | pass | pass | Validated 2026-08-09 on Android 17 through an ephemeral `cloudflared` HTTPS tunnel: account and API-key authentication, full catalogue sync, native MP3/FLAC Range playback, Opus at 64 kbit/s, album favorite, scrobbles and playlist create/update. |
| Feishin | 1.15.1 | pass | pass | pass | pass | Validated 2026-08-02: native playback, Opus cache, playlist create/add, favorite, rating, scrobble and queue. |
| Substreamer | 8.0.91 | pass | pass | pass | pass* | Validated 2026-08-02 from the official release source: native playback, Opus/128 cache, playlist add, favorite, rating and scrobble. `*` Its playback queue is local-only and the client never calls `getPlayQueue`/`savePlayQueue`; those endpoints remain covered by fixtures and Feishin. |
| DSub | 5.5.3 (F-Droid 208) | pass | pass | pass | pass* | Validated 2026-08-02 on Android 14, then revalidated 2026-08-15 on the current Android environment: authentication, catalogue, artwork, native playback, seek and playlist. `*` Scrobbling was disabled in the original run; the endpoint remains covered by fixtures and other clients. |
| Juliet | iOS build tested 2026-08-15 | pass | pass | native pass; transcode not run | not run | Current iOS compatibility check: authentication, catalogue, artwork and native playback succeeded. Unsupported/unexercised surfaces are not inferred as passes. |
| Symfonium | 14.1.0 | pass | pass | pass | pass | Re-run 2026-08-19 against the current contract, on an Android 17 emulator through an ephemeral `cloudflared` HTTPS tunnel. Every observable change of the six batches was checked against server state rather than against the client's own display: sleeve order in both the album and a synced playlist, two distinct album artists with no composite entity in the artist list, two album genres, a genre spelled four ways answering as one (4 tracks), the extended fields including `explicitStatus`, and a failed login decoded correctly now that it arrives as HTTP 200. User data round-tripped: track and album ratings, track/album/artist favorites, 21 scrobbles, a bookmark resumed at 37 s, three playlists owned by the authenticated user, and 13 Opus cache entries across the 64 and 128 kbit/s ceilings. The earlier 2026-08-09 run on a physical Android 17 device additionally covered API-key authentication, which this one did not re-run. |
| Feishin | 1.15.1 | pass | pass | pass | pass | Re-run 2026-08-19 against the current contract, on Windows desktop. **This run found the two server defects fixed in the same change**: a cold transcode refused the `Range: bytes=0-` that a browser sends to open any resource, so playback failed on every track no earlier client had transcoded; and `createPlaylist` appended its `songId` values instead of replacing the list, so every playlist edit looked lost. Both were then replayed against the fixed server through a logging proxy and confirmed from server state: cold transcode, cache hit and seek all answer, a removal drops to one track, a reorder lands. The rest of the run exercised search (23 `search3`), 145 cover-art reads, genre browsing including 8 `getSongsByGenre`, artists, `getStarred`, `getRandomSongs`, 9 scrobbles, favorites on tracks/album/artists and ratings — all read back from the server rather than from the client. `getAlbumInfo`/`getAlbumInfo2` were never called by this version, so they stay unexercised here rather than counted as a pass, as do the bookmark and play-queue methods. `getTopSongs`, `getArtistInfo` and `getInternetRadioStations` answer empty containers, which is what this server has to say about them. |
| Substreamer | 8.0.91 | pass | pass | pass | pass* | Validated 2026-08-02 from the official release source: native playback, Opus/128 cache, playlist add, favorite, rating and scrobble. `*` Its playback queue is local-only and the client never calls `getPlayQueue`/`savePlayQueue`; those endpoints remain covered by fixtures, and by Feishin's 2026-08-02 run. |
| DSub | 5.5.3 (F-Droid 208) | pass | pass | pass | pass | Re-run 2026-08-19 against the current contract, on an Android 17 emulator, read from `logcat -s RESTMusicService` and DSub's own disk cache rather than from its interface. **This run found the album-artist defect fixed in the same change**: an album credited to two artists hung off a third entity named after the joined string, and browsing to either real artist found no album. The rest passed: sleeve order in both browse modes, two album genres, one canonical `Jazz` answering 4 tracks, artwork, native FLAC and MP3 with seek, a failed login decoded as an error now that it arrives as HTTP 200, playlist create/add/remove/rename/delete — the removal going through `songIndexToRemove`, by index — and a rating. It settles a question Symfonium left open: DSub does submit the scrobble of the last track in a queue. Two of its three unique surfaces needed the brief corrected. The legacy `getAlbumList` is used **only** when "Browse By Tags" is off, `getAlbumList2` otherwise; both were exercised. And `maxBitRate` is never sent above the source — DSub emits `min(setting, track bitrate)` — so "ceiling above the source" is not expressible from this client; the two reachable cases are correct (128 ceiling on a 128 kbit/s MP3 streams natively, 64 transcodes). The generic `star?id` form resolved a track, an album and an artist, all three surviving a resync. Unresolved, and not a server fault: one FLAC (`Drift`) is fetched repeatedly and never completed by the client, though the server answers 200 with the exact `content-length` and the bytes arrive md5-identical to the source file through the same tunnel. Absent from the client, recorded as absent: the explicit marker, and any separate display of two album artists. |
| Juliet | 1.5 (iOS 26.6) | pass | pass | pass | pass | Re-run 2026-08-19 against the current contract, on a physical iPhone — the previous entry was a narrower check that never reached transcoding or user data. **This run found the second half of the cold-transcode defect**: iOS AVFoundation opens a resource with a `bytes=0-1` probe, which the first fix still classified as a seek and refused, so the first play of each track failed and the second succeeded off the cache the failed request had built. Fixed in the same change and verified by replaying the client's exact sequence: `bytes=0-1` cold answers 200, warm answers 206 with two bytes, a real seek answers 206, and a seek into a cold transcode is still refused. The rest of the run: 306 cover-art reads, 115 streams, 64 scrobbles, `getAlbumList2`, `search3`, `getRandomSongs`, `getSongsByGenre`, `getSimilarSongs2`, `getLyrics`, favorites, and playlist create/update/delete. It is also the first client in the replayed set to call `savePlayQueue`/`getPlayQueue` - 48 times - where the matrix otherwise rests on fixtures and on Feishin's historical 2026-08-02 run, its 2026-08-19 re-run having never called them. Absent from the client, recorded as absent: Juliet has no rating feature, so `setRating` was never sent; and it saves its queue with `position=0` every time, so playback restarts at zero — the server stores and returns what it is given, queue, current track and `changedBy` included. |

**Every row above predates the move to HTTP 200 on every `/rest` answer.** Each
records a real run against the previous status behaviour and is kept as a
historical result; none of them validates the current Subsonic contract. The M3
real-client gate was closed on that earlier behaviour, so it is not a satisfied
gate for the next tag: every client must be re-run against the current contract
and its row re-dated first. Missing client features stay covered by automated
fixtures and another real client rather than inferred as passes. Creating any tag
or release remains a separate action requiring an explicit operator request.
**Symfonium, Feishin, DSub and Juliet were all re-run on 2026-08-19 against the
current contract**, each on a real device or desktop and each read from server
state rather than from the client's own display. Substreamer is out of the
replayed set for the reason given below, and its row remains a historical
result against the previous status behaviour.

The replay found **four server defects**, all fixed in the same change and none
of which the automated suite could have produced: it took a browser, a client
that edits a playlist by sending back what remains, a client that browses by
artist index, and an iOS player that probes a resource before reading it.
Missing client features stay covered by automated fixtures and another real
client rather than inferred as passes.

Feishin and Juliet found their defect rather than passing it, so each one
re-played the fixed path itself rather than being credited on a replayed
request sequence: Feishin's MP3 transcode now starts, caches and seeks, and
Juliet's two-byte probe answers 200 cold with no refusal left in the log. The
five rows therefore record the behaviour of the server as it stands. Creating
any tag or release remains a separate action requiring an explicit operator
request.

The Substreamer row records the successful 2026-08-02 run. It could not be
reinstalled on the current Android 17 device during the 2026-08-15 revalidation
because the store marks that legacy build incompatible; Juliet provides the
current iOS sanity check instead. The historical Substreamer evidence is kept,
not silently rewritten as a new run.
because the store marks that legacy build incompatible, and the 2026-08-19
re-run confirmed it: that build no longer launches on the current emulator. It
is therefore **out of the replayed set** — its row stays as historical evidence
of a real run against the old contract, and is not counted toward the next tag.
Juliet on a physical iPhone takes its place as the fifth client, which also
moves the iOS check off an emulator. The historical Substreamer evidence is
kept, not silently rewritten as a new run.

For Substreamer, the Android Media3 session completed the 11-track validation album and the server recorded the corresponding start/submission scrobbles. With the client's streaming profile set to Opus at 128 kbit/s, WaveFlow produced 11 distinct cache entries; `ffprobe` identified the output as an Ogg container with an Opus audio stream. Its playlist mutation, track/album favorites and 4/5 rating were also read back from WaveFlow rather than inferred from local UI state.

Expand All @@ -37,12 +53,11 @@ The contract audit additionally covers administrative folder access: `createUser

Protocol failures now answer HTTP 200 and report the outcome in the body
(`status="failed"` plus an `error` code), as the Subsonic contract requires.
Previously WaveFlow also set 401, 403, 404, 409 or 429. All five clients in the
matrix above were validated against that old behaviour. Each reads `error/code`,
so none is expected to break — but that is an expectation, not a result, and the
matrix records runs rather than inferences: **every row must be re-run and
re-dated before the next tag**. Range responses keep 206/416, and `/share` and
`/api/v2` are unchanged.
Previously WaveFlow also set 401, 403, 404, 409 or 429. That this would not
break the clients was an expectation until the 2026-08-19 replay made it a
result: **each re-run row above was given a deliberate wrong password and
decoded it as an authentication error**. Range responses keep 206/416, and
`/share` and `/api/v2` are unchanged.

The same release adds `tokenInfo` — the half of `apiKeyAuthentication` that was
advertised but never served — and `getAlbumInfo`/`getAlbumInfo2`, so Feishin
Expand All @@ -51,8 +66,8 @@ opens. `playlist.owner` and
`share.username` now carry the authenticated username instead of an empty
string, which is what Feishin reads to decide whether a playlist is editable.

Five further batches of wire changes have landed against the same unvalidated
matrix, and the re-run covers them all: media items gained the remaining
Five further batches of wire changes landed while the matrix was still
unvalidated, and the 2026-08-19 replay covers them all: media items gained the remaining
OpenSubsonic fields under the presence rule (`moods`, `explicitStatus`, `isrc`,
`replayGain`, `bpm` and the rest); `startScan`, `getScanStatus`, `search2`,
`getStarred` and the bookmark methods were added or backed by real state;
Expand Down
Loading
Loading