3f61079c02
Replaces the single 5GB capBytes with independent Liked + Rolling budgets (5GB each default). Bucket = liked-ness, NOT CacheSource: a cached track currently in the liked set is charged to / evicted under Liked; everything else is Rolling. Storage-only dedup — it never filters playback (S4's offline lists query the whole index). - db.dart: schema 8→9, AudioCacheIndex.lastPlayedAt (real play recency for S4 + rolling LRU; migration backfills to cachedAt). drift codegen run. - cache_settings: likedCapBytes + rollingCapBytes (+ setters); old cache_cap_bytes key dropped, defaults reapply (not data loss). - audio_cache_manager: touch(); bucketUsage() counts orphan partials (LockCaching files never indexed) as Rolling so the cap truly bounds disk; evictBuckets() drains non-liked LRU then sweeps orphans, Liked only by its own (large) cap — normal use never evicts the user's liked library. - prefetcher → evictBuckets with the cached liked set. - storage_section: two cap selectors + per-bucket usage (folds in S3 to avoid a broken intermediate). - Explicit Download dropped: removed album + playlist Download buttons, autoPlaylist pins, now-unused imports. - Tests updated/compiled (drift-cohort tests are CI-skipped). High blast radius (eviction deletes files) — liked-protective by design; needs operator device-check before "done". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
115 lines
4.1 KiB
Dart
115 lines
4.1 KiB
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
|
|
import '../auth/auth_provider.dart' show secureStorageProvider;
|
|
|
|
/// Operator-tunable cache settings. Persisted via
|
|
/// flutter_secure_storage on the same device.
|
|
///
|
|
/// #427 S2: the single `capBytes` is replaced by two independent
|
|
/// budgets — Liked and Rolling-recent — each defaulting to 5GB.
|
|
/// Bucketing is by liked-ness (a cached track currently in the
|
|
/// user's liked set is charged to Liked; everything else to
|
|
/// Rolling), so the dedup is storage-only and never filters
|
|
/// playback. The old `cache_cap_bytes` key is intentionally not
|
|
/// migrated; defaults reapply (a one-time re-tune, not data loss).
|
|
class CacheSettings {
|
|
const CacheSettings({
|
|
required this.likedCapBytes,
|
|
required this.rollingCapBytes,
|
|
required this.prefetchWindow,
|
|
required this.cacheLikedTracks,
|
|
});
|
|
|
|
/// Liked-bucket budget. 0 = unlimited.
|
|
final int likedCapBytes;
|
|
|
|
/// Rolling (recently-played) budget. 0 = unlimited.
|
|
final int rollingCapBytes;
|
|
|
|
/// 1..10. Number of next-tracks the prefetcher pre-downloads.
|
|
final int prefetchWindow;
|
|
|
|
/// When true, every like_track event triggers a pin with source autoLiked.
|
|
final bool cacheLikedTracks;
|
|
|
|
CacheSettings copyWith({
|
|
int? likedCapBytes,
|
|
int? rollingCapBytes,
|
|
int? prefetchWindow,
|
|
bool? cacheLikedTracks,
|
|
}) =>
|
|
CacheSettings(
|
|
likedCapBytes: likedCapBytes ?? this.likedCapBytes,
|
|
rollingCapBytes: rollingCapBytes ?? this.rollingCapBytes,
|
|
prefetchWindow: prefetchWindow ?? this.prefetchWindow,
|
|
cacheLikedTracks: cacheLikedTracks ?? this.cacheLikedTracks,
|
|
);
|
|
|
|
static const _fiveGiB = 5 * 1024 * 1024 * 1024;
|
|
|
|
static const defaults = CacheSettings(
|
|
likedCapBytes: _fiveGiB,
|
|
rollingCapBytes: _fiveGiB,
|
|
prefetchWindow: 5,
|
|
cacheLikedTracks: true,
|
|
);
|
|
}
|
|
|
|
class CacheSettingsController extends AsyncNotifier<CacheSettings> {
|
|
static const _kLikedCap = 'cache_liked_cap_bytes';
|
|
static const _kRollingCap = 'cache_rolling_cap_bytes';
|
|
static const _kPrefetch = 'cache_prefetch_window';
|
|
static const _kCacheLiked = 'cache_liked_tracks';
|
|
|
|
late FlutterSecureStorage _storage;
|
|
|
|
@override
|
|
Future<CacheSettings> build() async {
|
|
_storage = ref.read(secureStorageProvider);
|
|
final likedCap = await _storage.read(key: _kLikedCap);
|
|
final rollingCap = await _storage.read(key: _kRollingCap);
|
|
final pre = await _storage.read(key: _kPrefetch);
|
|
final liked = await _storage.read(key: _kCacheLiked);
|
|
return CacheSettings(
|
|
likedCapBytes: likedCap == null
|
|
? CacheSettings.defaults.likedCapBytes
|
|
: int.tryParse(likedCap) ?? CacheSettings.defaults.likedCapBytes,
|
|
rollingCapBytes: rollingCap == null
|
|
? CacheSettings.defaults.rollingCapBytes
|
|
: int.tryParse(rollingCap) ?? CacheSettings.defaults.rollingCapBytes,
|
|
prefetchWindow: pre == null
|
|
? CacheSettings.defaults.prefetchWindow
|
|
: (int.tryParse(pre) ?? 5).clamp(1, 10),
|
|
cacheLikedTracks: liked == null
|
|
? CacheSettings.defaults.cacheLikedTracks
|
|
: liked == 'true',
|
|
);
|
|
}
|
|
|
|
Future<void> setLikedCapBytes(int bytes) async {
|
|
await _storage.write(key: _kLikedCap, value: bytes.toString());
|
|
state = AsyncData(state.value!.copyWith(likedCapBytes: bytes));
|
|
}
|
|
|
|
Future<void> setRollingCapBytes(int bytes) async {
|
|
await _storage.write(key: _kRollingCap, value: bytes.toString());
|
|
state = AsyncData(state.value!.copyWith(rollingCapBytes: bytes));
|
|
}
|
|
|
|
Future<void> setPrefetchWindow(int n) async {
|
|
final clamped = n.clamp(1, 10);
|
|
await _storage.write(key: _kPrefetch, value: clamped.toString());
|
|
state = AsyncData(state.value!.copyWith(prefetchWindow: clamped));
|
|
}
|
|
|
|
Future<void> setCacheLikedTracks(bool on) async {
|
|
await _storage.write(key: _kCacheLiked, value: on.toString());
|
|
state = AsyncData(state.value!.copyWith(cacheLikedTracks: on));
|
|
}
|
|
}
|
|
|
|
final cacheSettingsProvider =
|
|
AsyncNotifierProvider<CacheSettingsController, CacheSettings>(
|
|
CacheSettingsController.new);
|