mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-19 01:17:26 +02:00
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:
+39
-1
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user