fix: stabilize library scan IDs, pause queue behavior, and scan race condition

- Generate stable SHA-1 based IDs for SAF-scanned library items to prevent null ID crashes on the Dart side
- Suppress false queue-complete notification when user pauses instead of finishing the queue, and break out of parallel loop immediately when paused with no active downloads
- Use SQLite as the single source of truth for library scan results to fix a race condition where auto-scan could fire before provider state finished loading, dropping unchanged rows
This commit is contained in:
zarzet
2026-03-17 23:54:49 +07:00
parent 66a89d9e8e
commit 5ccd06cc68
3 changed files with 50 additions and 12 deletions
+16 -4
View File
@@ -3248,6 +3248,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
}
_log.d('Concurrent downloads: ${state.concurrentDownloads}');
await _processQueueParallel();
final stoppedWhilePaused = state.isPaused;
_stopProgressPolling();
@@ -3273,7 +3274,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
_log.i(
'Queue stats - completed: $_completedInSession, failed: $_failedInSession, totalAtStart: $_totalQueuedAtStart',
);
if (_totalQueuedAtStart > 0) {
if (!stoppedWhilePaused && _totalQueuedAtStart > 0) {
await _notificationService.showQueueComplete(
completedCount: _completedInSession,
failedCount: _failedInSession,
@@ -3288,13 +3289,17 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
}
}
_log.i('Queue processing finished');
if (stoppedWhilePaused) {
_log.i('Queue processing paused');
} else {
_log.i('Queue processing finished');
}
state = state.copyWith(isProcessing: false, currentDownload: null);
final hasQueuedItems = state.items.any(
(item) => item.status == DownloadStatus.queued,
);
if (hasQueuedItems) {
if (hasQueuedItems && !state.isPaused) {
_log.i(
'Found queued items after processing finished, restarting queue...',
);
@@ -3310,8 +3315,15 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
while (true) {
if (state.isPaused) {
if (activeDownloads.isEmpty) {
_log.d('Queue is paused and no active downloads remain');
break;
}
_log.d('Queue is paused, waiting for active downloads...');
await Future.delayed(_queueSchedulingInterval);
await Future.any([
Future.wait(activeDownloads.values),
Future.delayed(_queueSchedulingInterval),
]);
continue;
}
+20 -6
View File
@@ -329,6 +329,11 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
if (items.isNotEmpty) {
await _db.upsertBatch(items.map((e) => e.toJson()).toList());
}
final persistedItems =
(await _db.getAll())
.map(LocalLibraryItem.fromJson)
.toList(growable: false)
..sort(_compareLibraryItems);
final now = DateTime.now();
try {
@@ -341,7 +346,7 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
}
state = state.copyWith(
items: items,
items: persistedItems,
isScanning: false,
scanProgress: 100,
lastScannedAt: now,
@@ -350,11 +355,11 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
);
_log.i(
'Full scan complete: ${items.length} tracks found, '
'Full scan complete: ${persistedItems.length} tracks found, '
'$skippedDownloads already in downloads',
);
await _showScanCompleteNotification(
totalTracks: items.length,
totalTracks: persistedItems.length,
excludedDownloadedCount: skippedDownloads,
errorCount: state.scanErrorCount,
);
@@ -439,8 +444,14 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
'$skippedCount skipped, ${deletedPaths.length} deleted, $totalFiles total',
);
// Build the incremental merge base from SQLite, not the current
// provider state. Startup auto-scan can fire before `state.items` has
// finished loading, which would otherwise drop unchanged rows from the
// in-memory library until a manual full rescan.
final existingJson = await _db.getAll();
final currentByPath = <String, LocalLibraryItem>{
for (final item in state.items) item.filePath: item,
for (final item in existingJson.map(LocalLibraryItem.fromJson))
item.filePath: item,
};
final existingDownloadedPaths = <String>[];
currentByPath.removeWhere((path, _) {
@@ -491,8 +502,11 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
_log.i('Deleted $deleteCount items from database');
}
final items = currentByPath.values.toList(growable: false)
..sort(_compareLibraryItems);
final items =
(await _db.getAll())
.map(LocalLibraryItem.fromJson)
.toList(growable: false)
..sort(_compareLibraryItems);
final now = DateTime.now();
try {