Files
minstrel/flutter_client/lib/cache/metadata_prefetcher.dart
T
bvandeusen 4bd069430b feat(flutter): extend metadata prefetch to library tabs + artist detail
MetadataPrefetcher gains warmAlbums(ids) / warmArtists(ids) public
methods so callers can fan out drift-cache warm-ups for whatever
collection just landed.

Wired into:
- Library Artists tab — warms first 8 artists from the page on every
  data emit (initial + paginate + refresh).
- Library Albums tab — same for first 8 albums.
- Artist detail album grid — warms first 8 albums from the artist's
  album list as soon as it loads, so tapping into any of them is a
  drift hit.

Hard-cap of 8 per call (same as the home prefetch). Set spans across
calls aren't deduped at this layer because providers themselves
short-circuit on cached values.

Also instrument the artist-detail play button: try/catch around the
artistTracksProvider read + playTracks call, snackbar on
empty-tracks or thrown-error so silent failures stop being silent.
The current behavior was an early return on tracks.isEmpty with no
visible feedback.
2026-05-11 18:52:37 -04:00

99 lines
3.4 KiB
Dart

import 'package:flutter/foundation.dart' show debugPrint;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../library/library_providers.dart';
import '../models/home_data.dart';
/// Pre-warms the drift cache for entities the user is most likely to
/// tap. The home prefetch fires when /api/home returns. Library
/// tabs and artist detail expose `warmAlbums` / `warmArtists` so they
/// can fan out reads from their own AsyncNotifier callbacks.
///
/// All reads are unawaited and idempotent — providers that already
/// have a cached value short-circuit. Readers/writers don't conflict
/// because each provider de-duplicates concurrent subscribers.
class MetadataPrefetcher {
MetadataPrefetcher(this._ref) {
_ref.listen<AsyncValue<HomeData>>(homeProvider, (_, next) {
next.whenData(_warmHome);
});
}
final Ref _ref;
/// Cap per section so a 50-album recently-added row doesn't fan
/// out 50 fetches the moment the screen appears. Top-N covers what
/// the user can see without scrolling on a typical phone.
static const _topN = 8;
/// Pre-warm drift for each album id (no-op past the first dedup).
/// Called from the library Albums tab + artist detail album grid.
void warmAlbums(Iterable<String> ids) {
var n = 0;
for (final id in ids) {
if (id.isEmpty) continue;
if (n++ >= _topN) break;
_swallow(_ref.read(albumProvider(id).future));
}
if (n > 0) debugPrint('metadataPrefetcher: warming $n albums');
}
/// Pre-warm drift for each artist id.
void warmArtists(Iterable<String> ids) {
var n = 0;
for (final id in ids) {
if (id.isEmpty) continue;
if (n++ >= _topN) break;
_swallow(_ref.read(artistProvider(id).future));
}
if (n > 0) debugPrint('metadataPrefetcher: warming $n artists');
}
void _warmHome(HomeData h) {
final albumIds = <String>{};
final artistIds = <String>{};
for (final a in h.recentlyAddedAlbums.take(_topN)) {
if (a.id.isNotEmpty) albumIds.add(a.id);
if (a.artistId.isNotEmpty) artistIds.add(a.artistId);
}
for (final a in h.rediscoverAlbums.take(_topN)) {
if (a.id.isNotEmpty) albumIds.add(a.id);
if (a.artistId.isNotEmpty) artistIds.add(a.artistId);
}
for (final ar in h.rediscoverArtists.take(_topN)) {
if (ar.id.isNotEmpty) artistIds.add(ar.id);
}
for (final ar in h.lastPlayedArtists.take(_topN)) {
if (ar.id.isNotEmpty) artistIds.add(ar.id);
}
for (final t in h.mostPlayedTracks.take(_topN)) {
if (t.albumId.isNotEmpty) albumIds.add(t.albumId);
if (t.artistId.isNotEmpty) artistIds.add(t.artistId);
}
debugPrint(
'metadataPrefetcher: warming ${albumIds.length} albums + ${artistIds.length} artists');
for (final id in albumIds) {
_swallow(_ref.read(albumProvider(id).future));
}
for (final id in artistIds) {
_swallow(_ref.read(artistProvider(id).future));
}
}
/// Discards the return value and any error from a fire-and-forget
/// provider read. We don't care about the value here — we only want
/// the side effect of writing drift.
void _swallow(Future<Object?> f) {
f.then<void>((_) {}).onError((_, __) {});
}
}
/// Read once at app start to activate the prefetcher (e.g. wire it
/// from a top-level Consumer or main.dart container override).
final metadataPrefetcherProvider = Provider<MetadataPrefetcher>((ref) {
return MetadataPrefetcher(ref);
});