0d171490c3
Cleared 5 errors + 1 warning + 1 info from CI flutter analyze on 5114a81:
- prefetcher.dart: Riverpod listener callbacks used (_, _) for two
placeholder params — Dart 3 enforces unique names. Changed to (_, __).
- prefetcher.dart: Riverpod 3's AsyncValue exposes `.value` (nullable)
not `.valueOrNull`. Three call sites updated.
- sync_controller.dart: doc comment had `?since=<cursor>` → analyzer
warned about unintended HTML. Wrapped in backticks with `{cursor}`.
- sync_controller.dart: delete loop over heterogeneous Table list
inferred as List<Table>; drift's delete() expects TableInfo. Unrolled
to explicit per-table deletes.
- audio_cache_manager_test.dart: db.into(...).insertAll([...]) doesn't
exist on InsertStatement — only insert/insertOnConflictUpdate. Used
db.batch((b) => b.insertAll(table, [...])) instead, in two test cases.
- audio_handler.dart: LockCachingAudioSource is marked experimental in
just_audio. Added // ignore: experimental_member_use — operator
acknowledged the experimental status during brainstorming and we're
the project; CI's --fatal-infos would otherwise gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
64 lines
2.3 KiB
Dart
64 lines
2.3 KiB
Dart
import 'package:audio_service/audio_service.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../player/player_provider.dart';
|
|
import 'audio_cache_manager.dart';
|
|
import 'cache_settings_provider.dart';
|
|
import 'db.dart';
|
|
|
|
/// Listens to the player's currently-playing track. When it changes,
|
|
/// computes the next-N tracks ahead in the queue and pins them via
|
|
/// AudioCacheManager (source: autoPrefetch). After pinning, runs an
|
|
/// eviction pass against the operator-set cap.
|
|
///
|
|
/// The window N comes from cacheSettingsProvider.prefetchWindow
|
|
/// (default 5, configurable in Settings).
|
|
class Prefetcher {
|
|
Prefetcher(this._ref) {
|
|
// The mediaItem stream changes whenever the active track changes
|
|
// (skipNext/skipPrev or natural progression). Settings changes (e.g.
|
|
// operator bumps prefetch window) also trigger a reconcile.
|
|
_ref.listen<AsyncValue<MediaItem?>>(mediaItemProvider, (_, __) => _reconcile());
|
|
_ref.listen<AsyncValue<CacheSettings>>(cacheSettingsProvider, (_, __) => _reconcile());
|
|
}
|
|
|
|
final Ref _ref;
|
|
|
|
Future<void> _reconcile() async {
|
|
final settings = _ref.read(cacheSettingsProvider).value;
|
|
if (settings == null) return;
|
|
|
|
final queue = _ref.read(queueProvider).value ?? const <MediaItem>[];
|
|
final current = _ref.read(mediaItemProvider).value;
|
|
if (queue.isEmpty || current == null) return;
|
|
|
|
final currentIdx = queue.indexWhere((m) => m.id == current.id);
|
|
if (currentIdx < 0) return;
|
|
|
|
final endIdx =
|
|
(currentIdx + settings.prefetchWindow).clamp(0, queue.length - 1);
|
|
|
|
final mgr = _ref.read(audioCacheManagerProvider);
|
|
|
|
for (var i = currentIdx; i <= endIdx; i++) {
|
|
final trackId = queue[i].id;
|
|
if (await mgr.isCached(trackId)) continue;
|
|
// Fire-and-forget; downloads happen in the background. Errors
|
|
// inside pin() are caught + cleaned up internally.
|
|
// ignore: unawaited_futures
|
|
mgr.pin(trackId, source: CacheSource.autoPrefetch);
|
|
}
|
|
|
|
// Eviction pass after pinning new files.
|
|
if (settings.capBytes > 0) {
|
|
await mgr.evict(targetBytes: settings.capBytes);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Read this provider once at app start to activate the prefetcher.
|
|
/// Constructor wires the listeners.
|
|
final prefetcherProvider = Provider<Prefetcher>((ref) {
|
|
return Prefetcher(ref);
|
|
});
|