mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-04 09:10:48 +02:00
fix(runtime): bound resources and quarantine stalled extensions
This commit is contained in:
@@ -32,7 +32,10 @@ import 'package:spotiflac_android/widgets/selection_action_button.dart';
|
||||
import 'package:spotiflac_android/widgets/selection_bottom_bar.dart';
|
||||
|
||||
class _AlbumCache {
|
||||
static final _cache = TtlCache<List<Track>>(const Duration(minutes: 10));
|
||||
static final _cache = TtlCache<List<Track>>(
|
||||
const Duration(minutes: 10),
|
||||
maxEntries: 40,
|
||||
);
|
||||
|
||||
static List<Track>? get(String albumId) => _cache.get(albumId);
|
||||
|
||||
|
||||
@@ -39,7 +39,10 @@ import 'package:spotiflac_android/widgets/view_queue_snackbar_action.dart';
|
||||
part 'artist_screen_widgets.dart';
|
||||
|
||||
class _ArtistCache {
|
||||
static final _cache = TtlCache<_CacheEntry>(const Duration(minutes: 10));
|
||||
static final _cache = TtlCache<_CacheEntry>(
|
||||
const Duration(minutes: 10),
|
||||
maxEntries: 24,
|
||||
);
|
||||
|
||||
static _CacheEntry? get(String artistId) => _cache.get(artistId);
|
||||
|
||||
|
||||
@@ -21,13 +21,14 @@ class _EmbeddedCoverCacheEntry {
|
||||
/// when the source file changed.
|
||||
class DownloadedEmbeddedCoverResolver {
|
||||
static const int _maxCacheEntries = 180;
|
||||
static const int _maxFailedExtractEntries = 360;
|
||||
|
||||
static final LinkedHashMap<String, _EmbeddedCoverCacheEntry> _cache =
|
||||
LinkedHashMap<String, _EmbeddedCoverCacheEntry>();
|
||||
static final Set<String> _pendingExtract = <String>{};
|
||||
static final Set<String> _pendingRefresh = <String>{};
|
||||
static final Set<String> _pendingPreviewValidation = <String>{};
|
||||
static final Set<String> _failedExtract = <String>{};
|
||||
static final LinkedHashSet<String> _failedExtract = LinkedHashSet<String>();
|
||||
|
||||
static String cleanFilePath(String? filePath) {
|
||||
if (filePath == null) return '';
|
||||
@@ -132,6 +133,15 @@ class DownloadedEmbeddedCoverResolver {
|
||||
}
|
||||
}
|
||||
|
||||
static void _rememberFailedExtract(String cleanPath) {
|
||||
_failedExtract
|
||||
..remove(cleanPath)
|
||||
..add(cleanPath);
|
||||
while (_failedExtract.length > _maxFailedExtractEntries) {
|
||||
_failedExtract.remove(_failedExtract.first);
|
||||
}
|
||||
}
|
||||
|
||||
static void _validateCachedPreviewAsync(
|
||||
String cleanPath,
|
||||
_EmbeddedCoverCacheEntry entry, {
|
||||
@@ -186,7 +196,7 @@ class DownloadedEmbeddedCoverResolver {
|
||||
final hasCover =
|
||||
result['error'] == null && await File(outputPath).exists();
|
||||
if (!hasCover) {
|
||||
_failedExtract.add(cleanPath);
|
||||
_rememberFailedExtract(cleanPath);
|
||||
_scheduleTempCoverCleanup(outputPath);
|
||||
return;
|
||||
}
|
||||
@@ -205,7 +215,7 @@ class DownloadedEmbeddedCoverResolver {
|
||||
}
|
||||
onChanged?.call();
|
||||
} catch (_) {
|
||||
_failedExtract.add(cleanPath);
|
||||
_rememberFailedExtract(cleanPath);
|
||||
_scheduleTempCoverCleanup(outputPath);
|
||||
} finally {
|
||||
_pendingExtract.remove(cleanPath);
|
||||
|
||||
@@ -1,22 +1,56 @@
|
||||
/// Simple in-memory cache where each entry expires after [ttl].
|
||||
import 'dart:collection';
|
||||
|
||||
/// Bounded in-memory LRU cache where each entry expires after [ttl].
|
||||
class TtlCache<T> {
|
||||
final Duration ttl;
|
||||
final Map<String, _TtlEntry<T>> _entries = {};
|
||||
final int maxEntries;
|
||||
final LinkedHashMap<String, _TtlEntry<T>> _entries =
|
||||
LinkedHashMap<String, _TtlEntry<T>>();
|
||||
|
||||
TtlCache(this.ttl);
|
||||
TtlCache(this.ttl, {this.maxEntries = 100}) {
|
||||
if (maxEntries <= 0) {
|
||||
throw ArgumentError.value(
|
||||
maxEntries,
|
||||
'maxEntries',
|
||||
'must be greater than zero',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
T? get(String key) {
|
||||
_removeExpired();
|
||||
final entry = _entries[key];
|
||||
if (entry == null) return null;
|
||||
if (DateTime.now().isAfter(entry.expiresAt)) {
|
||||
_entries.remove(key);
|
||||
return null;
|
||||
}
|
||||
// LinkedHashMap preserves insertion order, so reinserting makes this the
|
||||
// most recently used entry.
|
||||
_entries
|
||||
..remove(key)
|
||||
..[key] = entry;
|
||||
return entry.value;
|
||||
}
|
||||
|
||||
void set(String key, T value) {
|
||||
_removeExpired();
|
||||
_entries.remove(key);
|
||||
_entries[key] = _TtlEntry(value, DateTime.now().add(ttl));
|
||||
while (_entries.length > maxEntries) {
|
||||
_entries.remove(_entries.keys.first);
|
||||
}
|
||||
}
|
||||
|
||||
void remove(String key) => _entries.remove(key);
|
||||
|
||||
void clear() => _entries.clear();
|
||||
|
||||
int get length {
|
||||
_removeExpired();
|
||||
return _entries.length;
|
||||
}
|
||||
|
||||
void _removeExpired() {
|
||||
if (_entries.isEmpty) return;
|
||||
final now = DateTime.now();
|
||||
_entries.removeWhere((_, entry) => !now.isBefore(entry.expiresAt));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotiflac_android/widgets/app_bottom_sheet.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
@@ -36,19 +38,21 @@ class DuplicateReviewSheet extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _DuplicateReviewSheetState extends ConsumerState<DuplicateReviewSheet> {
|
||||
late final LocalLibraryNotifier _localLibraryNotifier;
|
||||
late Future<List<IsrcDuplicateGroup>> _groupsFuture;
|
||||
bool _deletedLocalRows = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_localLibraryNotifier = ref.read(localLibraryProvider.notifier);
|
||||
_groupsFuture = LibraryDatabase.instance.findIsrcDuplicateGroups();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_deletedLocalRows) {
|
||||
ref.read(localLibraryProvider.notifier).reloadFromStorage();
|
||||
unawaited(_localLibraryNotifier.reloadFromStorage());
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user