Commit Graph
100 Commits
Author SHA1 Message Date
austinried 6662bdd514 Replay pending actions when connectivity is restored
A raw network drop and recovery never flipped the offline flag, so the
queue only drained on the next sink op, an offline-mode toggle or a
restart. Observe the default network's availability and use it, together
with the persisted offline preference, to trigger a replay. This signal
only drains pending actions; it never changes offline mode.
2026-10-04 00:37:29 +00:00
austinried f20937c83e Harden the pending-action queue after review
- Order direct stars against the queue so a successful online unstar is no
  longer overwritten by an older queued star, and serialize all queue
  mutations under one lock.
- Treat a SubsonicException with a negative code as a transport failure:
  queue it in setStar and keep it on flush instead of dropping it.
- Make coalescing atomic, keep the offline collector alive across errors,
  and drain from the persisted offline flag so a queued action flushes on
  a cold start that is already online.
- Give ServerActionSink.flush a default body and drop the no-op overrides.
2026-10-04 00:37:29 +00:00
austinried 0e70268129 Queue stars and scrobbles while the server is unreachable
Actions that cannot be delivered are persisted in a pending_actions
table and replayed once the server is reachable again. Stars coalesce so
only the latest state per item is sent, and replay happens on the next
successful action, when offline mode is turned off, or before a sync.
2026-10-04 00:37:29 +00:00
austinried eb47748ee4 fdroid: address review findings
Query the target project for an existing MR (fork->upstream MRs live upstream), pin checkout to main, use a sha256 marker instead of a duplicated upstream copy, pass GL_TOKEN via GIT_ASKPASS instead of the push URL, use sed -E and check the upstream id parsed, and abort if upstream moves between the guard and the clone.
2026-10-04 00:30:08 +00:00
austinried 9b17920f7e fdroid: split into sync and submit workflows
Sync pulls fdroiddata's current recipe into tools/fdroid/ (refusing to clobber unsubmitted moves). Submit refuses when upstream has moved since the last sync or when there is nothing to submit, then pushes this repo's copy to the GitLab fork on a branch based on current upstream and opens the MR. Drops the API-overwrite script.
2026-10-04 00:30:08 +00:00
austinried 51c8e86c97 tools: read the upstream project id anonymously
A fine-grained token can be scoped to the fork only; fdroiddata is public, so its project id needs no auth.
2026-10-04 00:30:08 +00:00
austinried 5003c9fb48 tools: fix the F-Droid submit API calls
Use PUT for the repository file (it already exists on the fork), create the MR on the fork with target_project_id (the create endpoint ignores source_project_id), and reuse an open MR instead of deleting and recreating the branch. Drop the unused --branch/--title flags and the redundant workflow concurrency/defaults.
2026-10-04 00:30:08 +00:00
austinried fdb3e7ec0a tools: read the GitLab token from GL_TOKEN 2026-10-04 00:30:08 +00:00
austinried 774ac34bae tools: add a submit script for the F-Droid recipe
The fdroiddata recipe is now kept in tools/fdroid/ as the source of truth. tools/fdroid-submit.nu pushes it to the fork and opens the merge request via the GitLab API, so no fdroiddata clone or manual web editing is needed. Also runnable from a Forgejo Actions workflow_dispatch.
2026-10-04 00:30:08 +00:00
austinried 673e492765 docs: centre the screenshots and show five 2026-10-04 00:30:08 +00:00
austinried 94498d71e4 metadata: mirror the README feature list in the store description 2026-10-04 00:30:08 +00:00
austinried 4333522256 docs: rebuild the README for the native rewrite 2026-10-04 00:30:08 +00:00
austinried 5d9d9e5d7e brand: refresh the app icon, logo and feature graphic 2026-10-04 00:30:08 +00:00
austinried cdba272f98 metadata: harden the screenshot generator and changelog test
Only cache a cover after a successful response and drop the file if decoding
fails, so a transient error cannot poison the cache permanently. Fail with a
clear message instead of a bare NoSuchElementException when a demo album has
no songs or the artist list is empty, and normalize line endings before the
changelog length check.
2026-10-04 00:30:08 +00:00
austinried e34363c1ac metadata: show the cross-library search screenshot
Render the dedicated SearchScreen with results across playlists, artists and
albums instead of the album tab's in-list search.
2026-10-04 00:30:08 +00:00
austinried 8e6c1593dc metadata: say Subsonic-compatible and use lowercase subtracks
Drop the "API" from the server compatibility wording, and lowercase the brand
in the title, description, old changelog and the app name to match the logo.
2026-10-04 00:30:08 +00:00
austinried 4bf34ea20a Add a test for the 500-char changelog limit
Both F-Droid and Google Play cap release notes at 500 characters. Check every
metadata changelog in the unit test suite (which CI already runs), and trim the
two old Flutter changelogs that were over the limit.
2026-10-04 00:30:08 +00:00
austinried 3ead9f6fb6 metadata: make the 3.0 changelog plain text under 500 chars
Neither F-Droid nor Google Play renders Markdown in release notes, and both
cap them at 500 characters. Drop the Markdown header and bullets and trim the
copy to fit.
2026-10-04 00:30:08 +00:00
austinried 5bd63d44d7 metadata: only show albums and artists that have cover art
The demo library has plenty of entries with no artwork, which render as
placeholders in the store screenshots. Filter the album and artist lists (and
the home rows) to entries the sync marked with a coverArt id, so no extra
image loads are needed to decide.
2026-10-04 00:30:08 +00:00
austinried 468451c7cb metadata: tint library screens from the playing track
The library accent follows the playback artwork (LibraryRoute derives it from
the current item), so pass the playing album instead of the first list entry,
which made the tab bar disagree with the miniplayer. Inset the miniplayer
progress bar when the tab bar is present, matching AppRoot.
2026-10-04 00:30:08 +00:00
austinried 9976fbeb20 metadata: stack the miniplayer above the library tabs
Render the miniplayer in a column below the screen instead of overlaying it, so
the library tab bar and the list-options FAB are no longer covered. Use Ugress
for the artist screen, drop Various Artists from the home artist rows, and have
the album screen play its first track so the row indicator and the miniplayer
colours match the album.
2026-10-04 00:30:08 +00:00
austinried 0e96b5872c metadata: theme each screenshot and show the miniplayer
Use a different album for the now-playing and album screens (Chillhop Winter,
My latin way) so the store shots show distinct colour schemes, tint the home
screen with the playing album so the logo pops, and render the miniplayer on
the albums, album, artists and search screens. Search now runs "chill".

The generator stages the sync into an on-disk database and caches cover bytes
under the temp dir, and only fetches full-resolution art for the covers shown
large; grid art uses the thumbnail endpoint.
2026-10-04 00:30:08 +00:00
austinried 0410ed026c metadata: show the playing album tint on the home screenshot
Tint the home screen with the artwork theme the app derives from the playing
album, so the subtracks logo and accents pick up a vivid colour (The Knife -
Shaking The Habitual) instead of the default scheme. Switch the now-playing and
album screens to The Polish Ambassador - Future, Sex, Computers, and search for
"chill" so the result grid shows the Chillhop Essentials covers.
2026-10-04 00:30:08 +00:00
austinried 7815f211e4 metadata: render phone screenshots from the Navidrome demo library
Replace the placeholder-art screenshots with renders of a real library, so the
store listing shows real album and artist art and names.

The generator (DemoScreenshotTest, run with `gradle :app:demoScreenshots`) syncs
the public Navidrome demo (demo.navidrome.org) into an in-memory database using
the normal SyncService pipeline, derives the theme colours from the fetched
cover art, and renders each screen. The synced data and cover art stay in memory
and are never written to the repository; only the resulting PNGs are committed.
The test is excluded from the unit-test tasks and is not part of CI.

Also reverts the screenshot fixture palette tweak from the previous commit,
which the real art makes unnecessary.
2026-10-04 00:30:08 +00:00
austinried e7ee6fa9a2 release: bump to 3.0.0 and refresh F-Droid metadata
Bump versionCode to 13 and versionName to 3.0.0. The Flutter app ended at
2.0.0-alpha.3+12 (versionCode 12), so 13 keeps the eventual com.subtracks
release an in-place upgrade.

Metadata was still inherited from the Flutter app:
- add the changelog for versionCode 13
- rewrite the descriptions for the native app
- replace the Flutter phone screenshots with native renders, and mute the
  screenshot fixture palette so the placeholder artwork reads as covers
2026-10-04 00:30:08 +00:00
austinried 836d0636d1 search: tighten the filter bar's vertical padding 2026-10-04 00:30:08 +00:00
austinried f6e087b6ab search: list playlists before artists, albums and songs
Order the result sections smallest-first and apply the review fixes: pad
the filter bar for the navigation bar, keep at least one filter enabled,
hide the clear button on an empty field, drop the unused autoFocus and
bar-modifier parameters, and test the filter mask and section order.
2026-10-04 00:30:08 +00:00
austinried 5d81c2dc41 library: match the list options search rounding to the search field 2026-10-04 00:30:07 +00:00
austinried f54ea54566 search: outline the shared search field to match the list options entry 2026-10-04 00:30:07 +00:00
austinried 3bef469acc search: add a type filter bar and share the search field
Add a filter chip bar above the keyboard to toggle songs, albums, artists
and playlists independently, all on by default and scrolling when it
overflows. Extract one compact 48dp search field and use it in both the
cross-library screen and the library tabs.
2026-10-04 00:30:07 +00:00
austinried 12ad62813e search: localize the cross-library search strings 2026-10-04 00:30:07 +00:00
austinried b25b9deacd search: address review findings
Prefix result keys per type so ids shared across songs, albums, artists
and playlists cannot collide and crash the list; add a loading state so
a query in flight no longer reads as no results; gate on code points to
match SQLite's length(); bind the three-character floor to one constant;
label the search field for TalkBack; and drop the vacuous trigram-plan
test. Cancel the ViewModel scope in its test teardown so it cannot leak
into later Robolectric tests.
2026-10-04 00:30:07 +00:00
austinried c1081b4ae7 search: require three characters and play songs in album context
Drop the library-wide instr fallback: below three characters the
cross-type search now returns nothing instead of scanning every table,
and the screen asks for more input. Lock in that a song result plays its
album context rather than a search queue.
2026-10-04 00:30:07 +00:00
austinried 5a3d5ded78 search: expose cross-library search from a home FAB
Add FTS5-backed search across songs, albums, artists and playlists,
reachable from a search FAB on the Home tab. Terms of three characters
or more run through the trigram indices; shorter terms fall back to the
instr scan, matching the per-tab filters. Results are grouped by type
and route into detail screens or song playback.
2026-10-04 00:30:07 +00:00
austinried da57ac08af gradle: let AGP download missing SDK components 2026-10-04 00:30:07 +00:00
austinried 98a3d336a8 port the library tab labels from the old plural names
Albums/Artists/Playlists were new keys because the old Flutter strings
were plurals, so their translations went unused. Take the `other` form of
resourcesAlbumName/resourcesArtistName/resourcesPlaylistName into the three
plain-string resources for the 26-27 locales that translated them. Arabic
`resources_album_name` and Hungarian `resources_playlist_name` stay English
because their upstream `other` form is empty.
2026-10-04 00:30:07 +00:00
austinried 699a6e86f5 address review: localize connection errors, tighten i18n guard, drop dead resources
- AddSourceViewModel resolves SubsonicException.uiMessage too, so malformed/
  HTTP/truncated responses show localized text instead of the raw parser
  message; add a regression test.
- StringResourcesTest now requires an 'other' quantity in every plural and
  asserts exact placeholder (index and conversion) equality, both of which
  could previously pass while crashing getQuantityString at runtime.
- Map the pre-enum song_downloads.error sentences back to their DownloadError
  so existing failed rows keep their specific message.
- Localize the media-session star/unstar button label.
- Remove 12 unreferenced resources (queue name, filter year, sync_failed, and
  the unused codec_* strings; codec labels stay in AudioEncoding).
- Make buildTree's fallback arguments required; only tests used the defaults.
2026-10-04 00:30:07 +00:00
austinried 3162ac07d5 localize transient messages and the download notification
Route user-facing messages produced below the UI through a typed UiMessage
(string-resource id plus format args) instead of pre-built English strings:
showMessage, SyncStatus.Failed, playback errors and download toasts now
resolve in the current locale at the UI edge. Subsonic and page-cap
exceptions carry a UiMessage alongside their developer message, and the
download progress notification is formatted from resources.

Internal invariants and log tags stay plain strings.
2026-10-04 00:30:07 +00:00
austinried a1e45380cc leave the fixed ID3v1 genre names untranslated 2026-10-04 00:30:07 +00:00
austinried 24757bda3f localize user-facing text into string resources
Move every exposed UI string into res/values/strings.xml (+ plurals) and
read it via stringResource/pluralStringResource or getString, with 27
locale files carried over from the Flutter app's Weblate translations.
Only strings whose English is unchanged reuse an old translation; reworded
ones get fresh keys.

Persisted download failures become a DownloadError enum mapped to string
resources (stored by name, unknown legacy values degrade to Failed), the
per-app locale config is generated by AGP, and StringResourcesTest guards
locale keys and placeholders.
2026-10-04 00:30:07 +00:00
austinried 88cd3764bf align the swipe peek with the skip and harden its edges
Resolve previousItem/upcomingItem through the same offline download scan the transport uses, so the peeking art is the track the swipe will actually play. Clear a frozen strip when a fallback swipe fires, prefer the dragged direction over an opposite fling, and simplify the hand-back and ratio prefetch.
2026-10-04 00:30:07 +00:00
austinried 73add672e1 keep the previous artwork visible and stop the neighbour flashing mid-swipe
Swipe-back now goes to the previous track (previousTrack) instead of restarting, so the previous peek stays visible past the restart threshold. The frozen strip only hands back once the live neighbour props match it, so a stale neighbour can no longer flash while the slide settles.
2026-10-04 00:30:07 +00:00
austinried b4539b9ba5 peek the adjacent artwork while swiping the now playing screen
Resolve the previous/next queue items and render a three-slot cover strip that follows the drag, so the neighbouring cover slides in from the side on release. The strip is rotated optimistically at commit and hands back to live state once the controller reports the new track (or after a grace period), so a prop update mid-animation cannot desync it. Ratio prefetch and layered cover loading keep the incoming art from snapping as it loads.
2026-10-04 00:30:07 +00:00
austinried 317a8f4e09 swipe between tracks on the now playing screen
Left/right drags on the cover art skip to the next/previous track, mirroring the transport buttons; the cover follows the finger and settles back on release. Also drops the ripple on the title/artist block.
2026-10-04 00:30:07 +00:00
austinried 4ddbefe7cd downloads: address review on the cancel/progress rows
Name the cancel and delete buttons after the row so screen readers can
tell them apart, show an indeterminate bar when a running track has no
known total, and drop the no-op media refresh on cancel. Trim the dead
size alias and a redundant guard, and cover a queued row with no engine
id plus a failed track in the progress denominator.
2026-10-04 00:30:07 +00:00
austinried 196733f9a7 downloads: base album/artist progress on completed tracks
The aggregate summed bytes over only the currently active songs, so it
reset each time a track finished and the next one started. Count completed
tracks (plus the fraction of each running track) over the node's full track
count instead, so it climbs one track at a time.
2026-10-04 00:30:07 +00:00
austinried 5e074db834 downloads: show progress and cancel on album and artist rows too
A node with any active download now shows the aggregate progress bar and
cancels all its active downloads instead of offering delete; a node with
only finished downloads keeps the trash.
2026-10-04 00:30:07 +00:00
austinried 814331c402 downloads: show progress and a cancel button for in-progress downloads
In-progress rows on Manage downloads now render a determinate progress bar
and a cancel button; completed and failed rows keep the delete button.
Cancelling removes only the active rows, so any completed downloads stay
and can still be deleted.
2026-10-04 00:30:07 +00:00
austinried c3bfb612f8 address review: fix reveal anchoring and ID3v1 names
RevealNewFrontItem compared the post-anchor firstVisibleItemIndex against the
front threshold, so it only worked when the row was at index 0 and exactly one
item was prepended. Recover the pre-change position by subtracting the number
of items now before the old front, and cover the one-in and multi-prepend cases
with integration tests.

Adopt the canonical TagLib/mutagen names for two extended indices: 133 is
Worldbeat (was Negerpunk) and 148 is Abstract (was Christmas). Drop the unused
default on SongInfoDialog's genres argument.
2026-10-04 00:30:07 +00:00
austinried e8ac683b42 normalize every song genre into song_genres
Split genre tags on Navidrome's separators (semicolon, slash, comma) and
resolve ID3v1 numeric indices to their names, so a song's genres are one
deduped list. songs.genre is now derived data with no reader: drop the
column and its index, and read the song info dialog's genres from
song_genres.
2026-10-04 00:30:07 +00:00
austinried 31ed5064a5 home: reveal new front items and brick-pack genre chips
Scroll a recently played row back to the front when a new first item
appears while it is already at/near the start; the lazy layout otherwise
anchors the old first item and hides the new one. Balance the three
genre rows by estimated width and tighten their gap.
2026-10-04 00:30:07 +00:00
austinried ea74fce758 library: route the rediscover list through the download-free query too
Also assert the default pager still observes album writes and cover the
quoted-search path.
2026-10-04 00:30:07 +00:00
austinried c2acbd0eae downloads: skip redundant progress writes 2026-10-04 00:30:07 +00:00
austinried 487296e75e library: stop download writes from invalidating the browse lists
The album/artist/playlist paging SQL named song_downloads, so Room
invalidated the pages on every download progress tick, janking the list
and resetting scroll when a download started. Route the default (filter
off) path through a raw query that only observes its own table; the
downloaded/offline path keeps the download-aware queries.
2026-10-04 00:30:07 +00:00
austinried adc206337b settings: add icons to Manage downloads and Licenses 2026-10-04 00:30:07 +00:00
austinried 0cd6f30c88 address review: keep the notification tap when the activity is recreated 2026-10-04 00:30:07 +00:00
austinried b3cad97869 address review: signal now playing with an event flow, not an int 2026-10-04 00:30:07 +00:00
austinried fd3b1ce298 address review: keep notification taps working after recreation 2026-10-04 00:30:07 +00:00
austinried 46ab8f66c4 open now playing when the media notification is tapped 2026-10-04 00:30:07 +00:00
austinried 6eb23445f1 limit the home downloaded-songs preview in SQL 2026-10-04 00:30:07 +00:00
austinried 5dce881704 use the auto-mirrored TrendingUp icon 2026-10-04 00:30:06 +00:00
austinried 3e260aa170 address review: dedupe and trim genres, add a starred queue reference, fix the offline source switch 2026-10-04 00:30:06 +00:00
austinried 4779bfc88b keep playback running when offline mode is toggled, and swap the recently-added header icon 2026-10-04 00:30:06 +00:00
austinried bf180ef711 home: put a relevant icon on each section header 2026-10-04 00:30:06 +00:00
austinried 2061ebafca keep playback running when rebuilding the offline window, and order downloads newest-first 2026-10-04 00:30:06 +00:00
austinried 8848e951e4 play genres and downloads as lazy queue references instead of expanding every song 2026-10-04 00:30:06 +00:00
austinried adbd3b8d8e normalize every song's genres into song_genres and simplify the genre queries 2026-10-04 00:30:06 +00:00
austinried 3df16826e6 home: paginate the offline lists and add More to each downloaded section 2026-10-04 00:30:06 +00:00
austinried c44987ff31 home: swap to downloaded sections in offline mode 2026-10-04 00:30:06 +00:00
austinried 43c90c3453 home: tighten the genre chip rows 2026-10-04 00:30:06 +00:00
austinried d9aa18c37a home: play the whole genre list from a genre song row 2026-10-04 00:30:06 +00:00
austinried f62e8363de home: drop On repeat, restore starred, give artist lists the detail column 2026-10-04 00:30:06 +00:00
austinried e36a0e3de5 home: add an On repeat song row and move recently starred below genres 2026-10-04 00:30:06 +00:00
austinried b0a31d0291 home: show album more screens as lists with a section-relevant detail column 2026-10-04 00:30:06 +00:00
austinried 53aa0f71b3 home: add a little content gap and more space between sections 2026-10-04 00:30:06 +00:00
austinried 0bb7f684ba home: align the starred song rows with the image rows 2026-10-04 00:30:06 +00:00
austinried 27f2d752be home: normal-weight section headers flush with their content 2026-10-04 00:30:06 +00:00
austinried 50fab1566e home: smaller bold section headers with a tighter gap 2026-10-04 00:30:06 +00:00
austinried 141529d139 home: bold the section headers and tighten the header-to-content gap 2026-10-04 00:30:06 +00:00
austinried b62d6c13c9 home: larger section headers and even header-to-content spacing 2026-10-04 00:30:06 +00:00
austinried 4fff637699 home: bold headline wordmark with roomier top spacing 2026-10-04 00:30:06 +00:00
austinried e2c0511338 home: add the subtracks wordmark, brick-pack the genres in three scrollable rows, centre artist names 2026-10-04 00:30:06 +00:00
austinried ff236836a2 home polish: genres by most played in a wrapping flow, tighter album corners, spaced left-aligned artists 2026-10-04 00:30:06 +00:00
austinried 796e5e2827 queue the whole starred list from both starred song lists 2026-10-04 00:30:06 +00:00
austinried 3affe7e049 play the starred list from the home row, and reorder the home sections 2026-10-04 00:30:06 +00:00
austinried 82b62b8d9d fix the home tab text colour against the dark theme 2026-10-04 00:30:06 +00:00
austinried 378d6a3b47 add a home tab with local recommendation rows 2026-10-04 00:30:06 +00:00
austinried 111078353d trust user-installed CAs for self-signed servers
Port the Flutter app's network security config: add a base-config that trusts system and user certificate anchors and permits cleartext, and reference it from the application manifest. android:usesCleartextTraffic is now redundant and removed.
2026-10-04 00:30:06 +00:00
austinried b1e22d8395 point at the .forgejo readme and nix-home for runner details 2026-10-04 00:30:06 +00:00
austinried c857d5e910 record genre support in the integration test fixture
The Nextcloud genre exception was a comment; express it as a TestServer
capability instead, mirroring supportsTokenAuth.
2026-10-04 00:30:06 +00:00
austinried 26f58b7502 address review: share the test helpers, guard AppModuleTest cleanup
Use cancelAndJoinBlocking in the repo tests and AppModuleTest instead of hand-rolled runBlocking joins, put AppModuleTest's cleanup in a finally so a failed join cannot leave Main overridden and Koin started, and collapse the five polling await copies into one awaitUntil. upcomingItem now reads the queue snapshot under the lock but does the Room read outside it.
2026-10-04 00:30:06 +00:00
austinried 88b53821bd let tests own the worker coroutine scopes
The repositories and the playback controller each created a hidden SupervisorJob scope and exposed close() only so tests could join it before closing the DB; production never called close(). Inject the scope instead and have each test cancel and join the scope it passes, so no test-only teardown lives in the app. Koin now provides the app/io/playback scopes.

Also read upcomingItem under the controller lock, and give the wall-clock test waits one shared 30s budget instead of 5s per file: a loaded runner could starve the controller's single thread past the old deadline.
2026-10-04 00:30:06 +00:00
austinried 18dc929218 store multiple OpenSubsonic song genres
OpenSubsonic servers return a per-song <genres> array that was dropped,
leaving every song with only its first (or joined) legacy genre. Parse it
and store the ordered list in a song_genres table, keeping the single
genre column as the fallback for base Subsonic servers.

Album genres are derived by unioning the album's song genres, falling
back to the single song genre, so album-level genre never trusts the
per-server album genres[] behaviour.

Schema 21 -> 22, with parser, sync, unit and integration coverage.
2026-10-04 00:30:06 +00:00
austinried 227379d1d7 docs: mark phases 3 and 4 done and refresh the plan 2026-10-04 00:30:05 +00:00
austinried 010aa6d122 stop prefetching the next track's full artwork 2026-10-04 00:30:05 +00:00
austinried 4d4eb7b758 address review: count a play even when its scrobble fails
A submitted scrobble now counts locally regardless of the HTTP result
(offline already did), so a transient server error no longer drops the
play from the sorts. Last-played uses MAX(existing, at) so a backwards
clock step cannot move an album or artist back down the list.
2026-10-04 00:30:05 +00:00
austinried 95ac4ae6ce drop the sort auto-reset
It compared a possibly stale listQuery (the previous tab's value during a
tab switch) against the new tab's options and rewrote the new tab's query
to its default, so sorts appeared to reset. Not worth the fragility for a
hidden-option edge case.
2026-10-04 00:30:05 +00:00
austinried d3aaeca405 count a play locally when it is scrobbled
A submitted scrobble now bumps the song's playCount/played and its album
and artist ranks, so the play sorts move without waiting for a sync. The
sync then overwrites them with the server's own numbers, so the
increment cannot double-count.
2026-10-04 00:30:05 +00:00
austinried 06ca2da48d add frequently and recently played album and artist sorts
Song playCount/played are parsed and synced, then aggregated per album
(SUM/MAX) and per artist (through the album's artistId) after the song
sync. Albums and artists gain Frequent/Recent sorts with their own
indices; the options show only when the active source returned the
fields, and the aggregate only writes rows whose value changed.

The artist subqueries pin index_albums_sourceId_artistId: without it the
planner answers MAX(played) from index_albums_recent (sourceId, played),
which cannot apply the artistId equality, and scans every album once per
artist. The availability flows read the indexed album ranks rather than
scanning songs, which would re-run on every song-sync batch.
2026-10-04 00:30:05 +00:00
austinried a261612017 fade cover art over the letter placeholder and lift CJK initials 2026-10-04 00:30:05 +00:00