From d95d070565ccdf6e42957de3f5f62e242ddcd2f6 Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Sun, 6 Sep 2026 02:34:04 +0700 Subject: [PATCH] perf(library): stream normalized scan timestamp snapshots --- lib/providers/local_library_provider.dart | 6 ++- lib/services/library_database.dart | 53 ++++++++++++----------- test/library_scan_snapshot_test.dart | 39 +++++++++++++++++ 3 files changed, 71 insertions(+), 27 deletions(-) create mode 100644 test/library_scan_snapshot_test.dart diff --git a/lib/providers/local_library_provider.dart b/lib/providers/local_library_provider.dart index 1445fd51..6b85672e 100644 --- a/lib/providers/local_library_provider.dart +++ b/lib/providers/local_library_provider.dart @@ -759,7 +759,7 @@ class LocalLibraryNotifier extends Notifier { final useSnapshotBridge = Platform.isAndroid && existingFiles.isNotEmpty; final snapshotPath = useSnapshotBridge - ? await _db.writeFileModTimesSnapshot(sourceId: activeSourceId) + ? await _db.writeFileModTimesSnapshot(existingFiles) : null; Map result; @@ -1263,7 +1263,9 @@ class LocalLibraryNotifier extends Notifier { required Map existingFiles, }) async { final legacyPaths = existingFiles.entries - .where((entry) => entry.value <= 0) + // Negative timestamps deliberately force a metadata-version rescan; + // only a missing timestamp (zero) may be filled from the filesystem. + .where((entry) => entry.value == 0) .map((entry) => entry.key) .toList(); if (legacyPaths.isEmpty) { diff --git a/lib/services/library_database.dart b/lib/services/library_database.dart index 270ae359..4a1366f0 100644 --- a/lib/services/library_database.dart +++ b/lib/services/library_database.dart @@ -2064,14 +2064,11 @@ class LibraryDatabase { return result; } - Future writeFileModTimesSnapshot({String? sourceId}) async { - final db = await database; - final rows = await db.rawQuery( - 'SELECT file_path, COALESCE(file_mod_time, 0) AS file_mod_time, ' - 'audio_metadata_scan_version FROM library ' - '${sourceId == null ? '' : 'WHERE source_id = ?'}', - sourceId == null ? const [] : [sourceId], - ); + /// Uses the already normalized, backfilled timestamps from the scan caller. + /// Bounded writes avoid a second query and a library-sized StringBuffer. + Future writeFileModTimesSnapshot( + Map fileModTimes, + ) async { final tempDir = await getTemporaryDirectory(); final file = File( join( @@ -2079,24 +2076,30 @@ class LibraryDatabase { 'library_file_mod_times_${DateTime.now().microsecondsSinceEpoch}.tsv', ), ); - final buffer = StringBuffer(); - for (final row in rows) { - final path = row['file_path'] as String?; - if (path == null || path.isEmpty) continue; - final modTime = (row['file_mod_time'] as num?)?.toInt() ?? 0; - final scanVersion = - (row['audio_metadata_scan_version'] as num?)?.toInt() ?? 0; - buffer - ..write( - libraryIncrementalSnapshotModTime( - storedModTime: modTime, - storedScanVersion: scanVersion, - ), - ) - ..write('\t') - ..writeln(path); + try { + final output = await file.open(mode: FileMode.write); + try { + final buffer = StringBuffer(); + for (final entry in fileModTimes.entries) { + if (entry.key.isEmpty) continue; + buffer + ..write(entry.value) + ..write('\t') + ..writeln(entry.key); + if (buffer.length >= 64 * 1024) { + await output.writeString(buffer.toString()); + buffer.clear(); + } + } + if (buffer.isNotEmpty) await output.writeString(buffer.toString()); + await output.flush(); + } finally { + await output.close(); + } + } catch (_) { + if (await file.exists()) await file.delete(); + rethrow; } - await file.writeAsString(buffer.toString(), flush: true); return file.path; } diff --git a/test/library_scan_snapshot_test.dart b/test/library_scan_snapshot_test.dart new file mode 100644 index 00000000..391d8b9a --- /dev/null +++ b/test/library_scan_snapshot_test.dart @@ -0,0 +1,39 @@ +import 'dart:io'; +import 'package:flutter/services.dart'; +import 'package:spotiflac_android/services/library_database.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + test( + 'scan snapshot preserves normalized sentinel and backfilled timestamps across chunks', + () async { + final directory = await Directory.systemTemp.createTemp( + 'scan-snapshot-test-', + ); + const channel = MethodChannel('plugins.flutter.io/path_provider'); + final messenger = + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + messenger.setMockMethodCallHandler(channel, (_) async => directory.path); + addTearDown(() async { + messenger.setMockMethodCallHandler(channel, null); + await directory.delete(recursive: true); + }); + final timestamps = { + for (var i = 0; i < 4000; i++) '/music/歌曲 café $i.flac': i, + '/music/legacy.flac': -1, + '/music/backfilled.flac': 1720000000000, + }; + final path = await LibraryDatabase.instance.writeFileModTimesSnapshot( + timestamps, + ); + final lines = await File(path).readAsLines(); + expect( + lines, + timestamps.entries + .map((entry) => '${entry.value}\t${entry.key}') + .toList(), + ); + }, + ); +}