The pager's settled-page collector reports its initial value, so a cold start
wrote Albums back before the stored tab had been read - racing the very setting
this branch added, able to pin the app to Albums and overwrite the choice. The
screen waits for the stored tab before composing now, so the pager latches the
right page and the initial report matches it, which also drops the spurious
scroll and the search clear that came with it. The report is guarded anyway.
The data layer no longer names a UI tab from a coincidentally same-named enum:
it returns the raw stored value and the view model owns the Albums fallback.
The selected tab lived in rememberSaveable, which survives a rotation or a
process-death restore but not a cold start, so the app always came back on
Albums. It goes through DataStore now, like the per-tab sort and filters, which
were already durable - landing on a different tab than the one you had filtered
is what made the filters look reset.
Verify the Subsonic token hash and salt freshness, and fall back to per-album
songs when the empty-search probe fails with an I/O error instead of
aborting the whole sync.
Add a real-server sync test that mirrors navidrome and gonic into Room and
reads the rows back, plus SyncManager coverage for the success, coalescing
and mid-sync-failure paths.
Cover the sort mapping and delete/ping ViewModels, the download engine's
request flags, the queue helpers, download prefs and migration data
preservation.
Drop the leaked collectors in the ViewModel/queue tests, document the
screenshot tests as render smoke tests, bind a real search term in the
trigram plan test, and assert a positive in-flight invariant alongside the
concurrency timing checks.
The failure was only recorded as SyncStatus.Failed, which nothing shows, so a
bad username or an unreachable server folded silently. It goes through the same
toast the rest of the data layer uses, with the server's own message.
Only treat a rewind as a new listen when it lands in the first few
seconds, so a small seek-back no longer re-arms and duplicates a
scrobble. Fall back to the player's resolved duration when the queue
item has none, and stop the position flow publishing a value for a
track the state has not caught up to. Submit off the collector so an
offline write cannot stall counting.
Scrobbling can be turned off in Settings (on by default). Now-playing,
scrobbles and stars all go through a ServerActionSink so a persistent
offline outbox can replace the network implementation later without
touching the callers.
Send a now-playing notification when a track starts playing and one
submission once it passes half its duration or four minutes, using the
Subsonic scrobble endpoint's submission and time parameters.
A transcoded stream is chunked with no length, so the app declares an upper-bound
length, and on the first request the server is still producing the stream: a seek
near that bound lands past the data and comes back as POSITION_OUT_OF_RANGE
(Navidrome answers 416 until it has cached the transcode). That first request is
what warms the cache, so give the load one retry after a second: the retry sees a
real Content-Length and the stream plays and seeks. Further out-of-range errors
are given up on rather than looped.
A decoder often gives no bitrate, so the row vanished; for a downloaded file the
size over the track length fills it in. The AAC mime Android and media3 use is
audio/mp4a-latm, which read as "MP4A-LATM" rather than AAC.
The download and more blocks were different widths, so the play/shuffle pill
sat off centre and the download hugged its edge closer than the more button.
Both now take the same inset.
Streaming Opus from Navidrome failed the first play with a read out of range,
then worked. Ogg seeks by binary-searching for the last page from the end of the
file, and the length the app estimates for a transcode overshoots the real
output, so that search reads past the data and the server answers 416. Opus,
Ogg and Vorbis now get no estimate: seeking falls back to what is buffered, but
playback is not scuppered by a length we cannot know. Downloads were unaffected
because they never carried the estimate.
Deleting any download re-set the whole queue window, which makes the player
re-prepare the current item and audibly pauses. It now rebuilds only the items
whose local file appeared or went away, comparing what the window reports with
what the repository can resolve, and falls back to the full reload when the
window no longer lines up.
MediaExtractor reports audio/raw for some containers it decodes itself, so a
downloaded FLAC showed "Raw" while the same stream showed FLAC. The file's own
header is now the source of the format when the extractor gives nothing useful,
covering FLAC, MP3, AAC/ALAC, Ogg, Vorbis, Opus, WAV and WebM - and for the
containers, the codec inside them (a WebM is Opus or Vorbis, not "WebM"),
matching what media3 reports for the stream.
The info dialog's label column is a fixed width in the accent colour at a
smaller size, so the values line up. The quality rows read format then bitrate,
the manage row loses its subtitle, and the switches get explicit off colours,
since the themed defaults left the knob invisible against some backgrounds.
Downloads asked the platform for an unmetered network and nothing more, so
there was no way to run them on mobile. A setting under Downloads now decides,
defaulting to Wi-Fi only, and it also governs roaming: the platform permits
roaming by default, which would have contradicted the Wi-Fi-only choice.
The flag rides the enqueue request like the download quality does, cached in the
source repository so the poller does not read the preference per row.
The song context menus gain Info, which lists every field we hold for the song
and, when it is downloaded, the file's format, bitrate, sample rate, channels
and size read from the file itself. For the song currently playing it takes the
encoding from the media3 stream instead, so a transcoded stream reports what is
actually being played. The menu is shared, so this reaches the album, playlist,
now playing and queue menus at once.
The downloads manager shows the same format and bitrate beside the size on each
song row, read lazily for the rows on screen rather than for the whole tree.
Downloads always fetched the original file. The setting takes the same choices
as the stream quality, and when it transcodes the download uses the stream
endpoint with those parameters instead, so the stored file is already the
wanted encoding rather than the original that would be transcoded on each play.
A preference change applied before the source is ready falls back to original,
and the URI is built from the same cached source the stream URL uses.
A Subsonic server answers a refused download with HTTP 200 and an XML error
document, which the platform downloader saves like any payload, so the row went
Completed and playback then tried to decode the error page. No audio container
starts with markup, so the leading bytes tell them apart: a Completed download
that starts with < or { is failed instead, and a stored Completed row is
re-checked on reconcile so existing bad files get cleaned up too.