mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-12 02:37:18 +02:00
fix(library): preserve partial scans and ignore stale completion events
This commit is contained in:
1 parent
8aa8d598d8
commit
6c22a91498
6 files changed
+135
-39
No files matched your search
@@ -543,6 +543,8 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
|
||||
scanIsFinalizing: true,
|
||||
scanProgress: state.scanProgress >= 99 ? state.scanProgress : 99,
|
||||
scanCurrentFile: null,
|
||||
scannedFiles: scanFile.expectedCount,
|
||||
scanErrorCount: scanFile.errorCount,
|
||||
);
|
||||
Stream<Map<String, dynamic>> validatedRows() async* {
|
||||
var decodedRows = 0;
|
||||
@@ -561,7 +563,11 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
|
||||
}
|
||||
}
|
||||
|
||||
final result = await _db.replaceSourceStream(sourceId, validatedRows());
|
||||
final result = await _db.replaceSourceStream(
|
||||
sourceId,
|
||||
validatedRows(),
|
||||
preserveMissing: scanFile.errorCount > 0,
|
||||
);
|
||||
_log.i(
|
||||
'Stream-ingested ${result.inserted}/${scanFile.expectedCount} scan rows '
|
||||
'(${result.skipped} downloads excluded)',
|
||||
@@ -970,16 +976,23 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
|
||||
}
|
||||
|
||||
Future<void> _handleLibraryScanProgress(Map<String, dynamic> progress) async {
|
||||
if (_scanCancelRequested) return;
|
||||
// A newly subscribed native stream may still contain the previous scan's
|
||||
// terminal snapshot (including the initial idle snapshot). Only the scan
|
||||
// method's result can start finalization; never stop polling on that replay.
|
||||
if (_scanCancelRequested ||
|
||||
!state.isScanning ||
|
||||
state.scanIsFinalizing ||
|
||||
progress['is_complete'] == true) {
|
||||
return;
|
||||
}
|
||||
final nextProgress = (progress['progress_pct'] as num?)?.toDouble() ?? 0;
|
||||
final normalizedProgress = ((nextProgress * 10).round() / 10).clamp(
|
||||
0.0,
|
||||
100.0,
|
||||
);
|
||||
final isComplete = progress['is_complete'] == true;
|
||||
final displayProgress = isComplete
|
||||
final displayProgress = normalizedProgress >= 100.0
|
||||
? 99.0
|
||||
: (normalizedProgress >= 100.0 ? 99.0 : normalizedProgress);
|
||||
: normalizedProgress;
|
||||
final currentFile = progress['current_file'] as String?;
|
||||
final totalFiles = (progress['total_files'] as num?)?.toInt() ?? 0;
|
||||
final scannedFiles = (progress['scanned_files'] as num?)?.toInt() ?? 0;
|
||||
@@ -987,7 +1000,6 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
|
||||
|
||||
final shouldUpdateState =
|
||||
state.scanProgress != displayProgress ||
|
||||
state.scanIsFinalizing != isComplete ||
|
||||
state.scanCurrentFile != currentFile ||
|
||||
state.scanTotalFiles != totalFiles ||
|
||||
state.scannedFiles != scannedFiles ||
|
||||
@@ -995,9 +1007,8 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
|
||||
|
||||
if (shouldUpdateState) {
|
||||
state = state.copyWith(
|
||||
scanIsFinalizing: isComplete,
|
||||
scanProgress: displayProgress,
|
||||
scanCurrentFile: isComplete ? null : currentFile,
|
||||
scanCurrentFile: currentFile,
|
||||
scanTotalFiles: totalFiles,
|
||||
scannedFiles: scannedFiles,
|
||||
scanErrorCount: errorCount,
|
||||
@@ -1007,7 +1018,7 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
|
||||
if (_shouldShowScanProgressNotification(
|
||||
progress: normalizedProgress,
|
||||
totalFiles: totalFiles,
|
||||
isComplete: isComplete,
|
||||
isComplete: false,
|
||||
)) {
|
||||
await _showScanProgressNotification(
|
||||
progress: normalizedProgress,
|
||||
@@ -1016,10 +1027,6 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
|
||||
currentFile: currentFile,
|
||||
);
|
||||
}
|
||||
|
||||
if (isComplete) {
|
||||
_stopProgressPolling();
|
||||
}
|
||||
}
|
||||
|
||||
void _stopProgressPolling() {
|
||||
|
||||
@@ -3,6 +3,26 @@ import 'package:sqflite/sqflite.dart';
|
||||
import 'package:spotiflac_android/utils/file_access.dart';
|
||||
import 'package:spotiflac_android/utils/logger.dart';
|
||||
|
||||
/// Removes rows superseded by a staged scan inside the caller's transaction.
|
||||
/// On partial scans, an unreadable file is not evidence that it was deleted.
|
||||
Future<void> deleteReplacedLibraryScanRows(
|
||||
DatabaseExecutor db,
|
||||
String sourceId, {
|
||||
required String stageTable,
|
||||
required bool preserveMissing,
|
||||
}) async {
|
||||
final where = preserveMissing
|
||||
? 'source_id = ? AND (id IN (SELECT id FROM $stageTable) '
|
||||
'OR file_path IN (SELECT file_path FROM $stageTable))'
|
||||
: 'source_id = ?';
|
||||
await db.rawDelete(
|
||||
'DELETE FROM library_path_keys WHERE item_id IN '
|
||||
'(SELECT id FROM library WHERE $where)',
|
||||
[sourceId],
|
||||
);
|
||||
await db.rawDelete('DELETE FROM library WHERE $where', [sourceId]);
|
||||
}
|
||||
|
||||
/// Deletes a selection atomically without exceeding SQLite's parameter budget.
|
||||
Future<void> deleteLibraryItemsByIds(Database db, Iterable<String> ids) async {
|
||||
final uniqueIds = ids.toSet().toList();
|
||||
|
||||
@@ -1059,6 +1059,7 @@ class LibraryDatabase {
|
||||
String sourceId,
|
||||
Stream<Map<String, dynamic>> items, {
|
||||
int batchSize = 300,
|
||||
bool preserveMissing = false,
|
||||
}) async {
|
||||
if (batchSize <= 0) {
|
||||
throw ArgumentError.value(batchSize, 'batchSize', 'Must be positive');
|
||||
@@ -1148,15 +1149,11 @@ class LibraryDatabase {
|
||||
final selectedColumns = columns.map((column) => 's.$column').join(', ');
|
||||
|
||||
await db.transaction((txn) async {
|
||||
await txn.rawDelete(
|
||||
'DELETE FROM library_path_keys WHERE item_id IN '
|
||||
'(SELECT id FROM library WHERE source_id = ?)',
|
||||
[sourceId],
|
||||
);
|
||||
await txn.delete(
|
||||
'library',
|
||||
where: 'source_id = ?',
|
||||
whereArgs: [sourceId],
|
||||
await deleteReplacedLibraryScanRows(
|
||||
txn,
|
||||
sourceId,
|
||||
stageTable: _scanStageTable,
|
||||
preserveMissing: preserveMissing,
|
||||
);
|
||||
await txn.rawDelete('''
|
||||
DELETE FROM library_path_keys
|
||||
|
||||
@@ -30,10 +30,12 @@ Object? _decodeJsonFileInBackground(String path) {
|
||||
class LibraryScanNDJSONFile {
|
||||
final File file;
|
||||
final int expectedCount;
|
||||
final int errorCount;
|
||||
|
||||
const LibraryScanNDJSONFile({
|
||||
required this.file,
|
||||
required this.expectedCount,
|
||||
this.errorCount = 0,
|
||||
});
|
||||
|
||||
Stream<Map<String, dynamic>> rows() async* {
|
||||
@@ -2344,11 +2346,6 @@ class PlatformBridge {
|
||||
errors != errors.toInt()) {
|
||||
throw FormatException('$method returned an invalid error count');
|
||||
}
|
||||
if (errors > 0) {
|
||||
throw FormatException(
|
||||
'Library scan could not read $errors files; the existing Library was kept',
|
||||
);
|
||||
}
|
||||
final pathValue = result['path'];
|
||||
final countValue = result['count'];
|
||||
if (pathValue is! String || pathValue.trim().isEmpty) {
|
||||
@@ -2366,7 +2363,11 @@ class PlatformBridge {
|
||||
if (!await file.exists()) {
|
||||
throw FormatException('$method did not create its output file');
|
||||
}
|
||||
return LibraryScanNDJSONFile(file: file, expectedCount: count);
|
||||
return LibraryScanNDJSONFile(
|
||||
file: file,
|
||||
expectedCount: count,
|
||||
errorCount: errors.toInt(),
|
||||
);
|
||||
} catch (_) {
|
||||
// Keep partial output; native SAF scan resumes from its sidecar.
|
||||
rethrow;
|
||||
|
||||
Reference in new issue
Block a user