refactor: optimize SAF metadata reading, CUE sibling resolution, and startup initialization

- Add fast-path SAF metadata reading via /proc/self/fd with displayNameHint support, falling back to temp copy
- Replace repeated findFile() CUE audio sibling lookups with cached case-insensitive directory listing
- Cache parsed CUE sheets to avoid redundant parsing during library scans
- Optimize incremental scan CUE modTime lookup from O(N*M) to O(N+M)
- Defer local library provider loading until localLibraryEnabled setting is true
- Replace O(n) track+artist history lookup with O(1) map-based lookup
- Delay startup maintenance tasks by 2s to reduce launch-time contention
This commit is contained in:
zarzet
2026-03-12 03:36:48 +07:00
parent fc1567d2c8
commit f1eef47600
7 changed files with 429 additions and 206 deletions
+39 -1
View File
@@ -8,6 +8,7 @@ import 'package:spotiflac_android/providers/download_queue_provider.dart';
import 'package:spotiflac_android/providers/extension_provider.dart';
import 'package:spotiflac_android/providers/library_collections_provider.dart';
import 'package:spotiflac_android/providers/local_library_provider.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/services/notification_service.dart';
import 'package:spotiflac_android/services/share_intent_service.dart';
import 'package:spotiflac_android/services/cover_cache_manager.dart';
@@ -89,14 +90,51 @@ class _EagerInitialization extends ConsumerStatefulWidget {
}
class _EagerInitializationState extends ConsumerState<_EagerInitialization> {
ProviderSubscription<bool>? _localLibraryEnabledSub;
bool _localLibraryPreloaded = false;
@override
void initState() {
super.initState();
_initializeAppServices();
_initializeExtensions();
ref.read(downloadHistoryProvider);
ref.read(localLibraryProvider);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_initializeDeferredProviders();
});
}
@override
void dispose() {
_localLibraryEnabledSub?.close();
super.dispose();
}
void _initializeDeferredProviders() {
ref.read(libraryCollectionsProvider);
_maybePreloadLocalLibrary(
ref.read(
settingsProvider.select((settings) => settings.localLibraryEnabled),
),
);
_localLibraryEnabledSub = ref.listenManual<bool>(
settingsProvider.select((settings) => settings.localLibraryEnabled),
(previous, next) {
if (next == true) {
_maybePreloadLocalLibrary(true);
}
},
);
}
void _maybePreloadLocalLibrary(bool enabled) {
if (!enabled || _localLibraryPreloaded) return;
_localLibraryPreloaded = true;
ref.read(localLibraryProvider);
_localLibraryEnabledSub?.close();
_localLibraryEnabledSub = null;
}
Future<void> _initializeAppServices() async {
+63 -33
View File
@@ -205,6 +205,7 @@ class DownloadHistoryState {
final List<DownloadHistoryItem> items;
final Map<String, DownloadHistoryItem> _bySpotifyId;
final Map<String, DownloadHistoryItem> _byIsrc;
final Map<String, DownloadHistoryItem> _byTrackArtistKey;
DownloadHistoryState({this.items = const []})
: _bySpotifyId = Map.fromEntries(
@@ -218,8 +219,25 @@ class DownloadHistoryState {
items
.where((item) => item.isrc != null && item.isrc!.isNotEmpty)
.map((item) => MapEntry(item.isrc!, item)),
),
_byTrackArtistKey = Map.fromEntries(
items
.map(
(item) => MapEntry(
_trackArtistKey(item.trackName, item.artistName),
item,
),
)
.where((entry) => entry.key.isNotEmpty),
);
static String _trackArtistKey(String trackName, String artistName) {
final normalizedTrack = trackName.trim().toLowerCase();
if (normalizedTrack.isEmpty) return '';
final normalizedArtist = artistName.trim().toLowerCase();
return '$normalizedTrack|$normalizedArtist';
}
bool isDownloaded(String spotifyId) => _bySpotifyId.containsKey(spotifyId);
DownloadHistoryItem? getBySpotifyId(String spotifyId) =>
@@ -231,16 +249,9 @@ class DownloadHistoryState {
String trackName,
String artistName,
) {
final normalizedTrack = trackName.trim().toLowerCase();
final normalizedArtist = artistName.trim().toLowerCase();
if (normalizedTrack.isEmpty) return null;
for (final item in items) {
if (item.trackName.trim().toLowerCase() == normalizedTrack &&
item.artistName.trim().toLowerCase() == normalizedArtist) {
return item;
}
}
return null;
final key = _trackArtistKey(trackName, artistName);
if (key.isEmpty) return null;
return _byTrackArtistKey[key];
}
DownloadHistoryState copyWith({List<DownloadHistoryItem>? items}) {
@@ -252,10 +263,12 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
static const int _safRepairBatchSize = 20;
static const int _safRepairMaxPerLaunch = 60;
static const int _audioMetadataBackfillMaxPerLaunch = 24;
static const _startupMaintenanceDelay = Duration(seconds: 2);
final HistoryDatabase _db = HistoryDatabase.instance;
bool _isLoaded = false;
bool _isSafRepairInProgress = false;
bool _isAudioMetadataBackfillInProgress = false;
bool _startupMaintenanceScheduled = false;
@override
DownloadHistoryState build() {
@@ -292,33 +305,45 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
state = state.copyWith(items: items);
_historyLog.i('Loaded ${items.length} items from SQLite database');
if (Platform.isAndroid) {
Future.microtask(() async {
await _repairMissingSafEntries(
items,
maxItems: _safRepairMaxPerLaunch,
);
await cleanupOrphanedDownloads();
await _backfillAudioMetadata(
state.items,
maxItems: _audioMetadataBackfillMaxPerLaunch,
);
});
} else {
Future.microtask(() async {
await cleanupOrphanedDownloads();
await _backfillAudioMetadata(
state.items,
maxItems: _audioMetadataBackfillMaxPerLaunch,
);
});
}
_scheduleStartupMaintenance(items);
} catch (e, stack) {
_historyLog.e('Failed to load history from database: $e', e, stack);
}
}
void _scheduleStartupMaintenance(List<DownloadHistoryItem> initialItems) {
if (_startupMaintenanceScheduled) {
return;
}
_startupMaintenanceScheduled = true;
unawaited(
Future<void>.delayed(_startupMaintenanceDelay, () async {
try {
if (Platform.isAndroid) {
await _repairMissingSafEntries(
initialItems,
maxItems: _safRepairMaxPerLaunch,
);
}
await cleanupOrphanedDownloads();
final currentItems = state.items;
if (currentItems.isNotEmpty) {
await _backfillAudioMetadata(
currentItems,
maxItems: _audioMetadataBackfillMaxPerLaunch,
);
}
} catch (e, stack) {
_historyLog.w('Startup history maintenance failed: $e');
_historyLog.d('$stack');
}
}),
);
}
String _fileNameFromUri(String uri) {
try {
final parsed = Uri.parse(uri);
@@ -1912,7 +1937,12 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
);
}
String addToQueue(Track track, String service, {String? qualityOverride, String? playlistName}) {
String addToQueue(
Track track,
String service, {
String? qualityOverride,
String? playlistName,
}) {
final settings = ref.read(settingsProvider);
updateSettings(settings);