Commit Graph
489 Commits
Author SHA1 Message Date
austinried cf92604e3a pin down that each tab keeps its own list query 2026-10-04 00:30:04 +00:00
austinried 270d749f0c remember the library tab across restarts
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.
2026-10-04 00:30:04 +00:00
austinried 684cc5a287 close the test gaps a QA pass found, and de-flake the flaky ones
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.
2026-10-04 00:30:04 +00:00
austinried 5cbe4c6839 tell the user when a sync fails
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.
2026-10-04 00:30:04 +00:00
austinried cdbecb9a9d drop unnecessary null checks flagged by the compiler 2026-10-04 00:30:04 +00:00
austinried 153122a552 fix scrobble double-count and missed plays found in review
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.
2026-10-04 00:30:04 +00:00
austinried 0379102e24 gate scrobbling behind a setting and funnel server writes through a sink
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.
2026-10-04 00:30:04 +00:00
austinried 29e5029b56 scrobble playback back to the server
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.
2026-10-04 00:30:04 +00:00
austinried d3425269ba Reapply "stop estimating a length for Ogg-based transcodes"
This reverts commit 2a3338be84cd728da799f549f144bc813611761a.
2026-10-04 00:30:04 +00:00
austinried c0d3ea767d Revert "retry a read that lands past a cold transcode"
This reverts commit 1aa92344ca02cd7a894cf8af4c9d06a9d49a853d.
2026-10-04 00:30:04 +00:00
austinried dd6b0ea00c retry a read that lands past a cold transcode
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.
2026-10-04 00:30:04 +00:00
austinried 9a999498ab show a bitrate the codecs do not report, and name LATM AAC
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.
2026-10-04 00:30:04 +00:00
austinried 7b43b73592 centre the info dialog's rows so the smaller labels line up with their values 2026-10-04 00:30:04 +00:00
austinried 76f3b39162 Revert "stop estimating a length for Ogg-based transcodes"
This reverts commit aeb08bf628cc98f81bd3084bce1a2a5da0a2e762.
2026-10-04 00:30:04 +00:00
austinried 73d38ddf3e even out the hero's side buttons around the play controls
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.
2026-10-04 00:30:04 +00:00
austinried b6a2f89d6f stop estimating a length for Ogg-based transcodes
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.
2026-10-04 00:30:04 +00:00
austinried c3f4969911 tighten the info dialog's label column so the values get the room 2026-10-04 00:30:04 +00:00
austinried 2459d7cb93 replace only the queue items a delete actually changed
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.
2026-10-04 00:30:04 +00:00
austinried eccf14aa58 name the codec, not the container, and tidy the info rows
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.
2026-10-04 00:30:04 +00:00
austinried caf8369924 let the user allow downloads on mobile data
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.
2026-10-04 00:30:03 +00:00
austinried 02b3ddbb98 add a song info dialog and show the encoding in the manager
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.
2026-10-04 00:30:03 +00:00
austinried adb9367f18 add a download quality preference
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.
2026-10-04 00:30:03 +00:00
austinried 26b8963205 fail a download whose payload is a server error page
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.
2026-10-04 00:30:03 +00:00
austinried 954d0eddd2 search the library through FTS5 trigram tables 2026-10-04 00:30:03 +00:00
austinried 46dc6bda0e select a server on tap and refuse to delete the active one 2026-10-04 00:30:03 +00:00
austinried a77ce6984e let a server be edited and move its delete into the edit screen 2026-10-04 00:30:03 +00:00
austinried e6b58b8f59 stop the poll loop and the notification on inactive waiting rows
The cross-source reconcile went too far: a source switched away from can have
waiting rows that promote will never hand over, since it resolves only while
active and downloads are only ever started for the active source. Those rows
kept the loop alive forever, so it scanned the whole table and ran every second
for the life of the process, and the notification sat there claiming to be
downloading songs that were not moving.

Both now count a row as in flight only when the engine owns it or it belongs to
the active source. Reconcile still covers every source, so a download running
for a source you switched away from keeps updating.

The downloaded-filter DAO test gains a second artist that must be excluded (its
first version passed whichever artist clause you wrote), and the preference test
covers the legacy three-field value and a downloaded round trip.
2026-10-04 00:30:03 +00:00
austinried 7d04125f58 add the Downloaded filter to the library tabs
One toggle per tab, persisted with the rest of the list query, narrowing to
entities that have at least one completed download. It rides the existing filter
SQL: every album, artist and playlist paging query takes a flag and the shared
filter constant adds an EXISTS over song_downloads, so it composes with the
sort, search and starred filter rather than being a separate mode.

The filters sheet now renders on every tab, since playlists support Downloaded
but not Starred.
2026-10-04 00:30:03 +00:00
austinried 7db6d1365f reconcile and notify downloads outside the active source
A source switched away from kept downloading on the platform while our poller
ignored it, so its rows froze as Running, the notification vanished and the only
Cancel went with it. The poll now reconciles every source's in-flight rows (the
engine reports status without a source) and the notification reads all of them,
which is also what cancelActive already acted on.

The grouped tab queries drop entities with nothing downloaded, since the UI
treats a missing entry and an all-zero one the same.

The indicator's two-value enum was used as a boolean, so it is one; and the
library screenshot now asserts the ring is drawn, rather than capturing an
image that would stay green if it disappeared.
2026-10-04 00:30:03 +00:00
austinried 47064e82f0 leave a blank line under the name in the delete prompt 2026-10-04 00:30:03 +00:00
austinried 6c09e7d786 put the name and the freed space on their own lines in the delete prompt 2026-10-04 00:30:03 +00:00
austinried 9bb2f7d8fc say space rather than sizes in the downloads settings row 2026-10-04 00:30:03 +00:00
austinried ce035f457b post our own download progress notification
The platform posts one notification per download, so queueing an album buries
the UI in them and they can be swiped away. Hiding them (VISIBILITY_HIDDEN,
which needs DOWNLOAD_WITHOUT_NOTIFICATION) and posting one aggregated
notification instead gives a single ongoing entry with the overall progress and
a Cancel action that stops everything in flight.

It is refreshed from the download state the poller already maintains, so it
tracks the engine without extra queries, and it is tied to this process: if we
are killed the downloads continue with no notification, which is the trade for
not writing a service.
2026-10-04 00:30:03 +00:00
austinried 20814a10f8 scrim the progress ring on album art only
The ring reads on the theme background of the artist and playlist rows but got
muddled over cover art, so the album tiles and the artist page's album cells put
it back on a translucent surface circle; the rows stay bare.
2026-10-04 00:30:03 +00:00
austinried b324cbb6b9 show only an in-flight ring on the library surfaces
A finished download no longer draws anything on album covers, artist rows or
playlist rows: the badge is the progress ring alone, so a fully downloaded tile
looks like any other. The ring takes the theme's primary for the arc over a
theme track, with the one soft shadow, instead of the white glyph and its
outline.
2026-10-04 00:30:03 +00:00
austinried dac56a7fcb group the downloads tree by the album's artist
The tree read the track artist off each song, so a downloaded album split into
one group per guest performer and every song without a track artist landed in
"Unknown artist". It reads the album's artist now, which is also the artist the
node's delete resolves through, so the two agree.

The badges are white glyphs with a dark outline and a progress ring with a
visible track, after a look at the rendered result; larger and clearer than the
first pass, which the art swallowed.
2026-10-04 00:30:03 +00:00
austinried 837ab9ac5d trim the play entries, calm the badge, and only watch the visible tab
The album and playlist menus lose Play and Shuffle and the song menu loses Play;
tapping the row or the detail hero buttons already covers those, so the
ItemActions play/shuffle callbacks and the artist view-model methods behind them
go too.

The badge was a filled circle that stood out over the art. It is now a white
glyph with a soft shadow in the icon's own shape, with no background behind it.

The library screen held all three grouped status flows at once, so the two tabs
off screen ran queries for badges nobody could see. It now collects only the
visible tab's map.
2026-10-04 00:30:03 +00:00
austinried b4f69affc9 put the mixed menu back to two rows, and only badge real progress
The split bar read badly; a partly downloaded list goes back to stacked
Download and Delete items.

The badge showed progress for any list with a downloaded song in it, which
made a mostly-downloaded album look like it was fetching. It now shows only
when that list is complete or has a download actually in flight; a partly
downloaded idle list shows nothing.

The album and playlist detail screens' more-menus had no download entries at
all, leaving delete reachable only via the hero's long press. They now take
the same status-driven items as everywhere else.
2026-10-04 00:30:03 +00:00
austinried 0f3dc45081 split a partly downloaded list's menu into one row
The mixed state offered two stacked rows; the spec wants one row divided
between Download and Delete. Everything else is a single full-width row.
2026-10-04 00:30:03 +00:00
austinried fedc502c3c add the downloads management screen
A Downloads screen reachable from Settings shows the active source's downloads
as an artist > album > song tree with per-node sizes and song counts, each node
collapsible. Artist and album nodes delete with a confirmation showing what they
free; song rows delete straight away, and a summary row offers Delete all.

Sizes come from the files on disk rather than the engine's reported total, so
they agree with the byte count the delete dialog already quotes. The tree is
built from one query joining the download rows to song, album and artist names.
2026-10-04 00:30:03 +00:00
austinried 7a0dd9aa5b show download status on the library tabs and offer it in their menus
Album tiles, artist rows and playlist rows now carry the badge (lower right on
tiles, trailing end on rows) and their long-press menus take the same bulk
Download / Cancel / Delete entries the hero uses, driven by the shared actions
rule so a partly downloaded item offers both rows and an in-flight one offers
Cancel. The artist page's album cells get the badge and album menu too.

The three list callbacks the artist menu used (and its artist-specific
view-model calls) collapse into one bulkDownload action over a MenuTarget, since
the same menu code now serves album, playlist and artist.
2026-10-04 00:30:03 +00:00
austinried edbcbeb380 group download status per entity for the library tabs
The detail heroes ask for one entity's status at a time. The list surfaces
need every status on screen at once, so add one grouped flow per tab that
returns a lookup of entity id to counts, which the tiles and rows read the way
song rows read the per-song map.
2026-10-04 00:30:03 +00:00
austinried d89b9e3a47 drop the concurrency dialog test 2026-10-04 00:30:03 +00:00
austinried 6f25f426ba fix the overlapping options in the concurrency dialog
ChoiceGroup emitted its header and option rows as siblings straight into the AlertDialog text slot, and they drew on top of each other, leaving only one radio visible with garbled labels. Wrap the group in its own Column so the rows stack, and cover it with a test that asserts the presets occupy separate rows.
2026-10-04 00:30:03 +00:00
austinried cf6ee00b6f move the artist header row down and the albums up
Drop the row's bottom padding and add to its top so the count and buttons sit closer to the album grid.
2026-10-04 00:30:03 +00:00
austinried 553752bf0a bump the artist album count text size
Use bodyLarge for the album count so it reads a little larger under the artist name.
2026-10-04 00:30:03 +00:00
austinried ea952e5a73 even out the artist album grid gutters
The columns were 16dp from the screen edges but 12dp apart, so the middle looked tighter than the sides. Use the same gap horizontally as vertically.
2026-10-04 00:30:03 +00:00
austinried 9f061e2a31 drop the artist biography placeholder and centre the header row
The 'Artist biography coming soon.' line is temporary and the bio work is shelved, so remove it. With only the album count left, centre it against the star and more buttons under the image (the buttons no longer need to sit below the hanging shuffle).
2026-10-04 00:30:03 +00:00
austinried 9ce7538e54 drop the cover thumbnail request to 384px to cover the common densities 2026-10-04 00:30:03 +00:00
austinried 74a91b2de0 request 512px cover thumbnails so the album grid stays sharp
main's 512px bump was reverted by bd748f5, which dropped it as a stray edit during the downloads rebase. Restore it.
2026-10-04 00:30:03 +00:00