From f437e0e70915fc1b582f6963aa759c076f971a00 Mon Sep 17 00:00:00 2001
From: InstaZDLL
Date: Wed, 16 Sep 2026 06:57:09 +0200
Subject: [PATCH 1/3] chore(release): 2.0.0-beta.1
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Six places carried the version, not the five the plan counted: `Cargo.toml`,
`docker-compose.yml`, the README three times — badge, Status line and the
`docker run` command — and the OpenAPI `info` block in `src/lib.rs`, which was
written out by hand.
That sixth one now reads `env!("CARGO_PKG_VERSION")`, so it cannot drift again.
Every other version string on the wire already derived from Cargo: the Subsonic
`serverVersion`, the health probe, the scrobbler user agent. This was the only
one that did not, and it is the one a release would have been most likely to
forget. Read back from a running instance: `info.version` is `2.0.0-beta.1`.
There was no CHANGELOG, so this adds one. It leads with the two `/api/v2`
response-shape changes, because the desktop app consumes that surface and
neither change breaks a compile: a reason code where `scrobble-destinations`
used to send an English sentence, and a `progress` frame on the scan event
stream that now speaks the same shape as the scan resource it describes.
The Status line records the 2026-09-16 replay against DSub and Symfonium. It
matters here because the façade was split by method family after the last tag,
and `CLAUDE.md` calls that contract frozen for the beta.
The four known issues are named rather than left to be rediscovered. All four
are present in `2.0.0-beta.0` as well, so none of them is a regression this
release introduces.
Signed-off-by: InstaZDLL
---
CHANGELOG.md | 116 +++++++++++++++++++++++++++++++++++++++++++++
Cargo.lock | 2 +-
Cargo.toml | 2 +-
README.md | 7 +--
docker-compose.yml | 2 +-
src/lib.rs | 2 +-
6 files changed, 124 insertions(+), 7 deletions(-)
create mode 100644 CHANGELOG.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..1190f604
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,116 @@
+# Changelog
+
+Notable changes to WaveFlow Server. Dates are the release date; the version is
+the one `Cargo.toml` carries and the tag `release.yml` refuses to publish if the
+two disagree.
+
+The desktop app at [`InstaZDLL/WaveFlow`](https://github.com/InstaZDLL/WaveFlow)
+consumes `/api/v2`, so anything that changes the shape of a response is called
+out first, whether or not it breaks a compile.
+
+## [2.0.0-beta.1] — 2026-09-16
+
+354 commits since `2.0.0-beta.0`.
+
+### Changed — `/api/v2` response shapes
+
+Two changes a native client must follow.
+
+- **`GET /api/v2/scrobble-destinations`** — the `unavailable` field now carries
+ a **case**, not an English sentence: `no_application_configured` or
+ `browser_journey_needs_https`. It was printed verbatim into a client that
+ ships in two languages, which told a French reader in English what to change
+ in their configuration. The wording now belongs to whoever is doing the
+ telling.
+
+- **`GET /api/v2/scans/{id}/events`** — the `progress` frame now speaks the same
+ shape as `GET /api/v2/scans/{id}`, where it used to send a second shape of the
+ same reading: `id`, `total_files` and `processed_files` instead of `scan_id`,
+ `total` and `processed`. A watcher that bound `total_files` showed `undefined`
+ from the first progress frame on. The type behind it is deliberately no longer
+ serialisable, so the other shape cannot come back.
+
+### Added
+
+- **External scrobbling.** ListenBrainz, Last.fm and Maloja, each a named
+ instance whose URL is part of its identity. A listen leaves by a durable
+ queue that survives a restart, is claimed before it is emitted, retries once,
+ honours a standard `Retry-After`, and reports a health that cannot read
+ `healthy` while three thousand listens wait. Last.fm authorises through a
+ browser, and a machine with no browser on it has its own journey.
+- **Canvas** — the looping visual a track can carry: its own store, six routes,
+ a ticket, an event, and a sweep that collects what a failed upload left behind.
+- **Uploads** — a library can be told to accept files; the server decides
+ whether it wants one, receives it in chunks under a bounded session, and makes
+ it a track.
+- **Track corrections** — a track's tags, artists and genres can be corrected
+ without rewriting the file. A correction leaves alone what it does not
+ mention, and an empty patch writes nothing and announces nothing.
+- **A library change feed** — `/api/v2` gained a change feed with a watermark a
+ device acknowledges for itself, a retention trim, and an answer that says
+ where the stream was cut.
+- **Subsonic album output fields** — `originalReleaseDate`, `releaseDate`,
+ `releaseTypes[]`, `recordLabels[]` and `discTitles[]` are answered where the
+ tags carry them.
+- **Search scoped to one library**, on both surfaces.
+- **The web client** grew the screens it was missing: uploads, canvas placement,
+ the track-tag editor, a live view of a running scan, API tokens, library
+ membership, sort and filter on the browse pages, player modes, and the four
+ gestures external scrobbling leaves to a client.
+
+### Changed
+
+- **The Subsonic façade is split by method family** rather than living in one
+ module, and the domain services are split by domain. Seven defects introduced
+ by the move were found and fixed before this release; the façade's observable
+ behaviour is unchanged except where this file says otherwise.
+- **A song is not a release** — a track no longer answers with fields that
+ belong to the album it sits on.
+
+### Fixed
+
+- **The transcode a seek abandons is now cached**, and a cache fill has a
+ deadline. A seek used to throw away the work it interrupted.
+- **Artist favourites survive a change of artist identity spec.**
+- **The track pid is a relocation hint**, asked only when path and content hash
+ have nothing to say, and dropped wholesale when the track spec changes — a
+ hint written under one spec must never be compared against a lookup computed
+ under another.
+- Numerous fixes across the scrobbling queue, the canvas store, the upload
+ session, the event feed's tenancy, and the web client's session handling.
+
+### Verified for this release
+
+- **The upgrade from `2.0.0-beta.0`**: the tagged binary built a database from a
+ real scan of 164 files, and the current binary opened it — **16 migrations,
+ all successful**, schema 22 → 38, catalogue and FTS5 search intact. Nothing in
+ the repository would have caught this: every integration target starts from an
+ empty database. Tracked by #223.
+- **The OpenSubsonic façade, replayed on 2026-09-16** against DSub 5.5.3 and
+ Symfonium 15.0.1 on an Android 17 emulator, with every result read back from
+ server state. Playback and seeking on cold transcodes, catalogue browsing,
+ the full playlist cycle, favourites, ratings, scrobbles and the server-side
+ play queue all behave. See
+ [the compatibility matrix](docs/subsonic-compatibility.md).
+
+### Known issues
+
+All of these are present in `2.0.0-beta.0` as well; none is a regression.
+
+- **#224** — a role separator cuts inside an unclosed parenthesis, so a composer
+ credit naming a parenthesised publisher becomes several artists.
+- **#225** — `getPlayQueue` and `getNowPlaying` name their children `song` where
+ the Subsonic schema says `entry`.
+- **#226** — a `416` on a cold transcode announces a total length of zero.
+- **#219** — a `401` from a session that has ended still renews and replays
+ under the next one.
+
+## [2.0.0-beta.0] — 2026-08-23
+
+First beta of WaveFlow Server v2: one axum binary, SQLite as the only database,
+FFmpeg for transcoding, an OpenSubsonic façade, and a React client compiled into
+the binary. The accepted design is
+[RFC-002](docs/rfcs/RFC-002-waveflow-server-v2.md).
+
+[2.0.0-beta.1]: https://github.com/InstaZDLL/waveflow-server/releases/tag/v2.0.0-beta.1
+[2.0.0-beta.0]: https://github.com/InstaZDLL/waveflow-server/releases/tag/v2.0.0-beta.0
diff --git a/Cargo.lock b/Cargo.lock
index 4b675e2f..9ae239f3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3455,7 +3455,7 @@ dependencies = [
[[package]]
name = "waveflow-server"
-version = "2.0.0-beta.0"
+version = "2.0.0-beta.1"
dependencies = [
"anyhow",
"argon2",
diff --git a/Cargo.toml b/Cargo.toml
index fa3759cc..5ea2826e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "waveflow-server"
-version = "2.0.0-beta.0"
+version = "2.0.0-beta.1"
description = "Self-hosted music server with WaveFlow and Subsonic compatibility."
authors = ["InstaZDLL "]
edition = "2021"
diff --git a/README.md b/README.md
index 291c8a7f..ca691c43 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
@@ -19,7 +19,7 @@ WaveFlow Server streams the music you already own to the clients you already use
No PostgreSQL. No Redis. No identity provider. No container orchestration. **One binary, one database file, one key file.**
-> **Status — `2.0.0-beta.0`.** All six milestones pass their release gates, and the OpenSubsonic façade has been replayed against four real clients on real devices — Symfonium, DSub, Feishin and Juliet — with every result read back from server state rather than from what the client displayed. See [the compatibility matrix](docs/subsonic-compatibility.md) for what each one actually exercises, and what it does not.
+> **Status — `2.0.0-beta.1`.** All six milestones pass their release gates, and the OpenSubsonic façade has been replayed against four real clients on real devices — Symfonium, DSub, Feishin and Juliet — with every result read back from server state rather than from what the client displayed. See [the compatibility matrix](docs/subsonic-compatibility.md) for what each one actually exercises, and what it does not.
## Install
@@ -32,7 +32,7 @@ docker run -d --name waveflow \
-p 4533:4533 \
-v waveflow-data:/data \
-v /path/to/music:/music:ro \
- ghcr.io/instazdll/waveflow-server:2.0.0-beta.0
+ ghcr.io/instazdll/waveflow-server:2.0.0-beta.1
```
Or with Compose — set `WAVEFLOW_MUSIC_PATH` to your music directory, which is mounted read-only:
@@ -116,6 +116,7 @@ Point any Subsonic client at `http://your-host:4533` with that username and pass
| **Run it** | [Operations](docs/operations.md) — backup, losing or rotating the instance key, reverse proxies, probes and logging |
| **Integrate** | [Native API v2](docs/api-v2-guide.md) · [Subsonic / OpenSubsonic](docs/subsonic-api-guide.md) |
| **What clients do** | [Compatibility matrix](docs/subsonic-compatibility.md) — the four replayed above, plus Substreamer as a historical row: that build no longer installs on a current device, so it is not counted toward a tag · [gap analysis](docs/opensubsonic-gap-analysis.md) |
+| **What changed** | [CHANGELOG](CHANGELOG.md) |
| **The design** | [RFC-002, the accepted design](docs/rfcs/RFC-002-waveflow-server-v2.md) · [RFC-003, sync](docs/rfcs/RFC-003-waveflow-sync-v2.md) · [RFC-004, local/server reconciliation](docs/rfcs/RFC-004-local-server-reconciliation.md) · [RFC-007, library events](docs/rfcs/RFC-007-library-event-stream.md) · [RFC-008, uploads](docs/rfcs/RFC-008-receiving-a-file.md) · [RFC-009, canvas](docs/rfcs/RFC-009-track-canvas.md) · [RFC-010, scrobbling](docs/rfcs/RFC-010-external-scrobbling.md) |
| **On a running server** | [`/reference`](http://127.0.0.1:4533/reference) for the interactive API · [`/openapi.json`](http://127.0.0.1:4533/openapi.json) for the contract |
| **Contribute** | [CONTRIBUTING.md](CONTRIBUTING.md) · [CLAUDE.md](CLAUDE.md) for the conventions in depth |
diff --git a/docker-compose.yml b/docker-compose.yml
index 19bb8438..d8fec391 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -3,7 +3,7 @@
# which is what a contributor wants and what an operator does not.
services:
waveflow:
- image: ghcr.io/instazdll/waveflow-server:2.0.0-beta.0
+ image: ghcr.io/instazdll/waveflow-server:2.0.0-beta.1
# build: .
restart: unless-stopped
ports:
diff --git a/src/lib.rs b/src/lib.rs
index aaba035e..e29158bf 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -103,7 +103,7 @@ fn chunk_body_limit(limits: &config::UploadLimits) -> usize {
#[openapi(
info(
title = "WaveFlow Server API",
- version = "2.0.0-beta.0",
+ version = env!("CARGO_PKG_VERSION"),
description = "Self-hosted WaveFlow music server v2.",
license(name = "AGPL-3.0-only")
),
From 8403419650192e561c9f31e26ef5e1667ac9f8be Mon Sep 17 00:00:00 2001
From: InstaZDLL
Date: Wed, 16 Sep 2026 19:04:24 +0200
Subject: [PATCH 2/3] docs(changelog): the four known issues are fixed, and one
of them changed the wire
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
They move into **Fixed**, and #225 moves further than that. Renaming
`getPlayQueue` and `getNowPlaying` children from `song` to `entry`, and giving
`playQueue` the `username` the schema requires, is an observable change for
anyone already reading `song` — so it belongs beside the two `/api/v2` shape
changes, not buried in a list of corrections. The section is no longer about
`/api/v2` alone.
It also records what was deliberately not done: both names are not emitted
together, because a response carrying `entry` and `song` conforms to neither
contract and would need a second wire change later to undo. And `getPlayQueue`
with nothing saved still answers a bare `playQueue`, which is not
schema-conforming either — making it so means omitting the element, on the one
call every client makes at startup, and that is a decision for after the beta.
The client campaign is recorded as what it was: the whole replayed set, four
clients on three platforms, not the two this file named. Substreamer is said to
be outside the set rather than left to look forgotten. And the commit count is
361, not 354.
Signed-off-by: InstaZDLL
---
CHANGELOG.md | 79 ++++++++++++++++++++++++++++++++++++++++------------
1 file changed, 61 insertions(+), 18 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1190f604..6ea41e22 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,11 +10,12 @@ out first, whether or not it breaks a compile.
## [2.0.0-beta.1] — 2026-09-16
-354 commits since `2.0.0-beta.0`.
+361 commits since `2.0.0-beta.0`.
-### Changed — `/api/v2` response shapes
+### Changed — response shapes
-Two changes a native client must follow.
+Three changes a client must follow — two on `/api/v2`, which the desktop app
+consumes, and one on the Subsonic façade.
- **`GET /api/v2/scrobble-destinations`** — the `unavailable` field now carries
a **case**, not an English sentence: `no_application_configured` or
@@ -30,6 +31,25 @@ Two changes a native client must follow.
from the first progress frame on. The type behind it is deliberately no longer
serialisable, so the other shape cannot come back.
+- **`getPlayQueue` and `getNowPlaying`** name their children **`entry`**, not
+ `song`. The Subsonic schema renames a media item inside those two containers
+ exactly as it does inside a playlist, and this server sent `song` in both
+ until now. A client that decodes against the schema read an empty queue and an
+ empty now-playing list; three client campaigns passed over it because every
+ client that met it was lenient. `playQueue` also gains the **`username`** the
+ schema requires beside `changed` and `changedBy`.
+
+ Both names are deliberately **not** emitted together: a response carrying
+ `entry` *and* `song` would conform to neither contract and would need a second
+ wire change later to undo. The freeze on the Subsonic contract protects the
+ clients that were validated against it, not a divergence from the
+ specification — which is what a beta is for.
+
+ `getPlayQueue` with nothing saved still answers a bare `playQueue`, unchanged
+ and not schema-conforming. Making it so means omitting the element, which is a
+ further wire change on the one call every client makes at startup; it is
+ pinned by a test and left to decide after the beta.
+
### Added
- **External scrobbling.** ListenBrainz, Last.fm and Maloja, each a named
@@ -69,6 +89,31 @@ Two changes a native client must follow.
### Fixed
+Four of these were found by the client campaigns of 2026-09-16 and cleared
+before the tag. All four shipped in `2.0.0-beta.0`; none is a regression this
+release introduces.
+
+- **A role separator no longer cuts inside parentheses** (#224). A `COMPOSER`
+ reading `Kobee (Melange / INHOUSE), Holy M (Melange / INHOUSE)` names two
+ people; the two slashes made three entities, each carrying a parenthesis it
+ never opened, and they reached the catalogue as artists that search answered
+ with. The reference's rule is untouched — `Bach/Gounod` is still two people
+ and `AC/DC` is still one band.
+- **`getPlayQueue` and `getNowPlaying` name their children `entry`** (#225), and
+ `playQueue` carries `username`. See the response-shape section above: this one
+ changes the wire.
+- **A `416` no longer announces a length of zero** (#226). A transcode still
+ being produced has no complete length, and `bytes */0` did not say "unknown" —
+ it said the resource is empty, which a client may believe and never ask about
+ again. The header is now omitted instead. A complete file, whose length *is*
+ known, states it and answers `Accept-Ranges: bytes`, where it used to claim
+ `none` and contradict every other answer for the same file.
+- **A refusal that outlived its session neither renews nor replays** (#219). A
+ sign-out and a sign-in fit inside one round trip, so a 401 raised for one
+ account could arrive after another had signed in — renewing spent the new
+ account's rotating refresh token for a request the old one made. Five routes
+ did this, including the scan event stream.
+
- **The transcode a seek abandons is now cached**, and a cache fill has a
deadline. A seek used to throw away the work it interrupted.
- **Artist favourites survive a change of artist identity spec.**
@@ -86,24 +131,22 @@ Two changes a native client must follow.
all successful**, schema 22 → 38, catalogue and FTS5 search intact. Nothing in
the repository would have caught this: every integration target starts from an
empty database. Tracked by #223.
-- **The OpenSubsonic façade, replayed on 2026-09-16** against DSub 5.5.3 and
- Symfonium 15.0.1 on an Android 17 emulator, with every result read back from
- server state. Playback and seeking on cold transcodes, catalogue browsing,
- the full playlist cycle, favourites, ratings, scrobbles and the server-side
- play queue all behave. See
- [the compatibility matrix](docs/subsonic-compatibility.md).
+- **The OpenSubsonic façade, replayed on 2026-09-16** against the whole
+ replayed set — **Symfonium 15.0.1** and **DSub 5.5.3** on an Android 17
+ emulator, **Juliet** on a physical iPhone, and **Feishin 1.15.1** on Windows
+ desktop — with every result read back from server state rather than from what
+ a client displayed. Playback and seeking on cold transcodes, catalogue
+ browsing, the full playlist cycle, favourites, ratings, scrobbles and the
+ server-side play queue all behave, and seeking by byte range was
+ re-established on iOS and on desktop. Substreamer is not part of the set: that
+ build no longer installs on the current Android device, and Juliet took its
+ place. See [the compatibility matrix](docs/subsonic-compatibility.md).
### Known issues
-All of these are present in `2.0.0-beta.0` as well; none is a regression.
-
-- **#224** — a role separator cuts inside an unclosed parenthesis, so a composer
- credit naming a parenthesised publisher becomes several artists.
-- **#225** — `getPlayQueue` and `getNowPlaying` name their children `song` where
- the Subsonic schema says `entry`.
-- **#226** — a `416` on a cold transcode announces a total length of zero.
-- **#219** — a `401` from a session that has ended still renews and replays
- under the next one.
+None outstanding. The four the client campaigns found were all present in
+`2.0.0-beta.0` as well, and all four were fixed before this tag rather than
+carried into it — #219, #224, #225 and #226, above.
## [2.0.0-beta.0] — 2026-08-23
From 1d8f66fc8420c96148732866be13f7352783c718 Mon Sep 17 00:00:00 2001
From: InstaZDLL
Date: Wed, 16 Sep 2026 19:26:54 +0200
Subject: [PATCH 3/3] docs: two claims that were wider than the facts behind
them
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
**The README said "four real clients on real devices".** Two of the four ran on
an Android 17 emulator. Only Juliet was on a physical iPhone; Feishin was on
Windows desktop. The line now names each client with where it actually ran,
because "on real devices" is the kind of claim a reader has no way to check and
every reason to take at face value.
**The CHANGELOG said "None outstanding".** Its own response-shape section, three
paragraphs above, describes a deviation left in place on purpose: `getPlayQueue`
with nothing saved answers a bare `playQueue`, carrying none of the `username`,
`changed` or `changedBy` the schema requires. A file cannot say both.
That deviation now has somewhere to live. #225 is closed and complete, so
pointing a pending decision at it would have buried it — #229 carries it
instead, with the two ways out stated and the reason neither was taken during a
release: conforming means *removing* the element, since the schema has no valid
empty form, and that is a wire change on the one call every client makes at
startup. The contract test that pins today's shape now names #229 too.
Signed-off-by: InstaZDLL
---
CHANGELOG.md | 16 ++++++++++++----
README.md | 2 +-
tests/subsonic_contract.rs | 2 +-
3 files changed, 14 insertions(+), 6 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6ea41e22..78af25e5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -48,7 +48,7 @@ consumes, and one on the Subsonic façade.
`getPlayQueue` with nothing saved still answers a bare `playQueue`, unchanged
and not schema-conforming. Making it so means omitting the element, which is a
further wire change on the one call every client makes at startup; it is
- pinned by a test and left to decide after the beta.
+ pinned by a test and left to decide after the beta, on #229.
### Added
@@ -144,9 +144,17 @@ release introduces.
### Known issues
-None outstanding. The four the client campaigns found were all present in
-`2.0.0-beta.0` as well, and all four were fixed before this tag rather than
-carried into it — #219, #224, #225 and #226, above.
+The four the client campaigns found were all present in `2.0.0-beta.0` as well,
+and all four were fixed before this tag rather than carried into it: they are
+listed above as #219, #224, #225 and #226.
+
+One deviation is knowingly left in place: **`getPlayQueue` with nothing saved
+answers a bare `playQueue`**, carrying none of the `username`, `changed` or
+`changedBy` the schema requires. The schema has no way to say "empty queue" —
+the reference omits the element — so conforming means removing it, which is a
+further wire change on the one call every client makes at startup. No client has
+been observed to mind. It is pinned by a contract test so it cannot drift
+unnoticed, and #229 carries the decision.
## [2.0.0-beta.0] — 2026-08-23
diff --git a/README.md b/README.md
index ca691c43..34574780 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ WaveFlow Server streams the music you already own to the clients you already use
No PostgreSQL. No Redis. No identity provider. No container orchestration. **One binary, one database file, one key file.**
-> **Status — `2.0.0-beta.1`.** All six milestones pass their release gates, and the OpenSubsonic façade has been replayed against four real clients on real devices — Symfonium, DSub, Feishin and Juliet — with every result read back from server state rather than from what the client displayed. See [the compatibility matrix](docs/subsonic-compatibility.md) for what each one actually exercises, and what it does not.
+> **Status — `2.0.0-beta.1`.** All six milestones pass their release gates, and the OpenSubsonic façade has been replayed against four real clients — **Symfonium 15.0.1** and **DSub 5.5.3** on an Android 17 emulator, **Feishin 1.15.1** on Windows desktop, and **Juliet** on a physical iPhone — with every result read back from server state rather than from what the client displayed. See [the compatibility matrix](docs/subsonic-compatibility.md) for what each one actually exercises, and what it does not.
## Install
diff --git a/tests/subsonic_contract.rs b/tests/subsonic_contract.rs
index 4d47c782..042e06b5 100644
--- a/tests/subsonic_contract.rs
+++ b/tests/subsonic_contract.rs
@@ -1652,7 +1652,7 @@ async fn play_queue_and_now_playing_name_their_children_entry() {
// schema has no way to say "empty queue" — the reference omits the element
// entirely — so conforming would mean a *third* observable change, on the
// one call every client makes at startup, with no client harm observed and
- // none of it asked for. Pinned rather than corrected, and recorded on #225.
+ // none of it asked for. Pinned rather than corrected, and carried by #229.
let unsaved = subsonic_json(&router, "getPlayQueue", "wfsk_queue", "").await;
let unsaved = &unsaved["subsonic-response"]["playQueue"];
assert!(unsaved.is_object(), "{unsaved}");