feat(cache): CacheFiller background metadata sweeper

Periodic worker that walks cached_artists for missing album lists
and cached_albums for missing track lists, then fills them via
/api/artists/:id + /api/albums/:id. Newly-discovered album covers
are pre-warmed into flutter_cache_manager's disk cache too.

Solves the "tap an artist → empty album area → pop in" experience:
artistAlbumsProvider was drift-first but the cache only got
populated when the user navigated TO an artist. SyncController's
/api/library/sync delta doesn't carry per-artist album lists (those
are query-time derived). Now the filler pre-populates them in the
background so the drift hit is real on first tap.

Pacing (intentionally conservative):
* 10-second initial delay so SyncController has time to land its
  first sync — the WHERE NOT EXISTS query has nothing to do until
  cached_artists is populated.
* 5-minute interval thereafter. Once steady state is reached the
  sweep is a cheap drift query + early exit.
* 200ms throttle between per-entity REST requests — never competes
  with active playback.
* 200 entities per sweep cap so a fresh install with thousands of
  artists doesn't tie up the network for one continuous run. Next
  sweep picks up where this one left off (NOT EXISTS naturally
  skips already-filled rows).

Wall time estimate for a 1000-artist library: ~3-4 minutes spread
over multiple sweeps. Single round-trip per artist (new
getArtistDetail API method returns artist + albums in one shot).

Activated from app.dart's postFrameCallback alongside the existing
SyncController / Prefetcher / MetadataPrefetcher / LiveEvents
hooks. Disposed via ref.onDispose when the provider scope tears
down (effectively process death in practice).
This commit is contained in:
2026-05-14 17:34:42 -04:00
parent fb95a462fb
commit 60085b1368
3 changed files with 260 additions and 0 deletions
+9
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'cache/cache_filler.dart';
import 'cache/metadata_prefetcher.dart';
import 'cache/prefetcher.dart';
import 'cache/sync_controller.dart';
@@ -39,6 +40,14 @@ class _MinstrelAppState extends ConsumerState<MinstrelApp> {
// arrive. Also installs an AppLifecycleState observer for
// resume-time defensive invalidation.
ref.read(liveEventsDispatcherProvider);
// Cache filler: background sweeper that walks cached_artists
// / cached_albums for missing relations and fetches the
// per-entity detail so tapping an artist surfaces albums
// immediately (drift hit, no /api/artists/:id round-trip at
// tap time). First sweep 10s after launch; every 5 minutes
// thereafter. Throttled 200ms between requests so it never
// competes with user activity.
ref.read(cacheFillerProvider);
});
}