-
v2026.08.18 — a lost file stops being a dead end, and the Sonos stops going quiet
Stableandroid / Build + lint + test (push) Successful in 4m16srelease / Build signed APK (tag releases only) (push) Successful in 5m25srelease / Build + push container image (push) Successful in 1m16srelease / Verify release artifacts (tag releases only) (push) Successful in 1sreleased this
2026-08-18 11:17:27 -04:00 | 0 commits to main since this releasePer-day CalVer. Server + web + Android, and there is a schema migration this time. Merged via PRs #126 and #127;
mainat4f077736, CI green across every lane that ran.Twenty-nine commits, and the largest single line in the diff is a deletion: the Flutter client is gone.
This is the first Android release since v2026.08.08. Thirteen Android commits have been sitting on
mainwith no way to reach a phone — everything below marked (app) arrives with this one.
A missing file is now a state, not a dead end
Minstrel already knew when a file vanished.
missing_sincegot set by the scanner, the selection paths quietly stopped picking those tracks, and that was the end of it — nothing else in the product knew, and nothing ever tried to get the file back.The original plan was to delete orphaned tracks. That was wrong, and you said so:
the intent of 'deleting' a track is to remove it from being played and to put the system in a state where something like lidarr will attempt to replace the file. we should be keeping records of the track the file was supposed to represent and all the history of how it was used.
So nothing is deleted. The row survives, and so do its plays, its likes and everything it contributed to your taste profile. The mark is the state, and the file is expected to come back.
In a playlist the entry stays where it was, greyed out and skipped rather than silently vanishing — a playlist that quietly shrinks is worse than one that shows you the hole. The server also refuses to hand out a stream URL for it, rather than handing one over and trusting every client to honour a flag.
In the admin UI, a new Missing files page groups by folder. The case that prompted this was three reorganised albums, which a flat list renders as forty unrelated problems. Each row carries when the track was last played, because "gone six months, never played" and "gone yesterday, played 200 times" deserve completely different reactions from you.
And it asks for the file back. An hourly sweeper turns missing albums into Lidarr requests on an exponential backoff. Two decisions do the safety work: it requests the album, not the track — three requests instead of forty — and it waits out a grace period first, because an unmounted volume fixes itself well inside a day and a trigger-happy system would flood your indexers over a loose cable.
⚠️ Auto-approve is ON by default. The first scan that finds an album missing past the grace window will create and approve a Lidarr request without asking. That is the intended behaviour, but it is the one setting here that acts on its own. Admin → Missing files → Re-acquisition turns it off if you would rather approve by hand. Decide before your next scan.
Everything is DB-backed and tunable from that card, and it states the resulting schedule in plain words — "first retry gap: 6" means nothing until you know it doubles.
Clients get told. This one nearly shipped inert.
/api/library/syncis a change-log feed, and marking a file missing is a plain UPDATE — so the flag existed on the wire and nothing ever asked a client to re-read the track. Reconcile now emits change rows, written before the mutation, because the two failure modes are not symmetric: a spurious re-read costs one fetch, a missed change row means that client never finds out, permanently.A fully-missing album also drops out of the Years axis now, which is what Genres already did. The two browse axes had quietly disagreed.
The Sonos
Two different faults, found in that order, both from one diagnostics dump.
It would stop, and nothing noticed (app)
A Sonos stopped by itself mid-track while the phone was asleep, and the app went on reporting "not playing" for minutes without acting. A renderer streams on its own — that is the point of casting — so a stream that dies is invisible unless something is watching. Now something is, and it retries before giving up and telling you.
It is deliberately timid about it: only a genuine stop, never a pause (a pause is usually a person at the Sonos app, and taking the transport back off them is a fight they always lose), and only after three polls agree.
It would stop because we gave it half a queue (app)
The better answer, found after you pushed back on my first one — I had been blaming the network, and you told me plainly to assume the app is where the problem lives and the only thing we should change. Re-read that way, the same dump pointed somewhere else.
The app loads your queue onto the Sonos one track at a time, and Sonos rate-limits rapid adds. The loader gave up after a few failures, logged a warning, and moved on. Nothing ever read the queue back. So the renderer would be holding ten tracks while the app believed there were forty, play its ten, and stop — correctly. That is not a stream dying; that is a speaker finishing exactly what it was handed.
It now asks the renderer how many tracks it actually has, and appends whatever is missing. And a stop is classified rather than assumed: a dead stream gets resumed, a short queue gets repaired and continues at the next track, and a queue that simply ended does nothing at all.
That last case matters more than it sounds. Reaching the end of a queue is how every listening session ends, and the watchdog above would have treated it as a fault — three retries and an error in your inbox every time you finished listening. It never reached a device, because it and its fix are both in this release.
And the stutter (app)
The play-pause-play-pause you have been describing at the start of a track. Two things here, and I want to be straight about which is which.
Something that was definitely wrong: while casting, the phone kept its own player paused rather than stopped — and a paused player keeps downloading. So your phone was pulling the same track the Sonos was streaming, over the same WiFi, starting fresh at every track change. FLAC, so roughly a second full-rate download competing with the speaker, beginning exactly when a new track begins. That is fixed.
Something I do not know yet: whether that was the cause. It fits the timing, but my previous theory about these symptoms fit too and was wrong.
So this release also instruments the symptom. The reason it has gone undiagnosed is that the diagnostics could not see it: the event that records player state does not include whether we are playing, the track-change event needs the queue position to move, and the heartbeat samples once every 45 seconds. A few seconds of oscillation at one track fell through all three, which is why you have described it several times and it has never once been measured.
Now a burst of transport changes writes a single event carrying the sequence, plus what the app and the Sonos each thought was playing at that moment. If those two disagree, it is our bug; if they agree while the speaker keeps rebuffering, it is the stream. Opposite fixes, and one captured episode tells us which — without you having to interpret anything.
Also
Genres and Years now exist in the Android Library (app), matching the web tab order — which closes the last browse gap between the two clients.
The dead strip above the MiniPlayer is gone (app). An unclaimed navigation-bar inset was leaking into all fifteen in-shell screens, plus 140dp of padding inherited from the Flutter era.
The Flutter client is deleted — 245 files, about 24,600 lines, last touched in May and built by nothing since its workflow was removed. Its design tokens were kept and moved to the repo root; they lived in that folder but were never its property.
Install
There is a migration this time (
0056_missing_reacquisition). Redeploy the server image and let migrations run.Update the Android app. This is the first APK in ten days and carries everything marked (app).
Then decide about auto-approve before your next scan — see the warning above.
What to look at afterwards
- Admin → Missing files. If it is empty, nothing is lost and there is nothing to do. If it is not, check the grouping matches how the files actually moved.
- The Re-acquisition card, and whether you want it approving on its own.
- Cast something to the Kitchen and let it run. That exercises the queue verification, the stall watchdog and the download fix all at once.
- Library → Genres / Years on the phone.
- Whether the miniplayer still hides the bottom of a list.
Known, and deliberate
The stutter fix is a candidate, not a diagnosis. If it persists, the new diagnostics will have captured it and that is the first thing to read.
The queue-truncation explanation is inferred, from the code and an old logcat, never confirmed against hardware. The loader now logs how many tracks the renderer actually holds on every cast, which settles it either way.
None of the Sonos work has been tested against a real speaker — there is no Sonos in CI, and there is no Lidarr in CI either, so the re-acquisition path has never made a real request. The quickest check is the admin Requests queue after a scan finds something past the grace window.
🤖 Generated with Claude Code
Downloads