feat: auto-sync the library on a timer while the app is open

While the app is in the foreground a timer checks the auto-sync setting and
syncs when due (default every 24h; the interval is selectable from 15 minutes
to 24 hours). It stops when the app is backgrounded. "Sync automatically"
is one setting: Off, Wi-Fi only (default) or Always.

A running sync is shown by a silent, dismissible notification and a small
spinner on the library tab bar's settings button. A manual pull-to-refresh
still holds the pull spinner; only an automatic sync does not. The library
and Home sync actions no-op while a sync is running, and SyncManager
coalesces a request that arrives mid-run into a single follow-up, so repeated
pull-to-refresh cannot pile up runs while adding a server still syncs it.

The time of the last sync attempt is recorded, so a failing server is retried
once per interval rather than every poll. The sync consumer and the auto-sync
loop both survive an unexpected failure instead of silently disabling syncing
for the rest of the process.

The poll sleeps for the interval (capped at 15 minutes) instead of a fixed
minute, the network mode is collected once rather than re-registering a
connectivity callback each poll, and the decision is logged at DEBUG (visible
with Verbose logging) rather than writing to the log file every poll.
This commit is contained in:
austinried
2026-10-10 03:36:16 +00:00
parent 8e7431cc2e
commit e396671ac7
17 changed files with 653 additions and 34 deletions
+1
View File
@@ -48,6 +48,7 @@ Everything comes from the Nix flake devshell; do not install toolchains by hand.
- Networking uses OkHttp and DOM XML parsing; auth uses the Subsonic token scheme by default.
- Use coroutines and `Flow`; library reads are exposed as `Flow` from Room. Large lists page with Paging 3 over Room `PagingSource` rather than loading the whole table.
- The library is mirrored in full at sync and must work offline. Never fetch library metadata on demand for browsing, filtering or sorting; only media bytes (streams, downloads) and images are on-demand. A server-derived sort key must be synced into a local column, not requested lazily when the sort is selected.
- Auto-sync is a foreground-only, in-process timer (`AutoSync`, driven by the Activity lifecycle), not a background worker: do not move it to WorkManager or an alarm. `SyncManager` coalesces a request that arrives while a sync is running into a single follow-up; the library and Home sync actions no-op while one is running.
- Playback is Media3: `PlaybackService` is a `MediaSessionService` owning an `ExoPlayer`, and `PlaybackController` (app-scoped Koin singleton) is the only thing the UI talks to. The queue is a `queue_entries` list of references with optional ranges plus a `playback_cursor` row; keep only a bounded window in memory (never the whole queue, and never load a whole table to build one). Stream URLs are freshly salted per request, so attach artwork by `CoverArtRef` cache key, not by URL.
- UI state lives in `androidx.lifecycle.ViewModel`, wired with Koin (`koinViewModel()`); screens split into a stateful `*Route` and a stateless `*Screen` for screenshot tests.
- UI and playback preferences go in DataStore (`UserPreferences`), not Room.