mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-15 21:28:20 +02:00
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:
@@ -30,6 +30,7 @@ import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.security.MessageDigest
|
||||
import java.util.Locale
|
||||
|
||||
class MainActivity: FlutterFragmentActivity() {
|
||||
@@ -111,6 +112,13 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildStableLibraryId(filePath: String): String {
|
||||
val digest = MessageDigest.getInstance("SHA-1")
|
||||
val bytes = digest.digest(filePath.toByteArray(Charsets.UTF_8))
|
||||
val hex = bytes.joinToString("") { "%02x".format(it) }
|
||||
return "lib_$hex"
|
||||
}
|
||||
|
||||
data class SafScanProgress(
|
||||
var totalFiles: Int = 0,
|
||||
var scannedFiles: Int = 0,
|
||||
@@ -1263,7 +1271,9 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
} else {
|
||||
try {
|
||||
val lastModified = try { doc.lastModified() } catch (_: Exception) { 0L }
|
||||
metadataObj.put("filePath", doc.uri.toString())
|
||||
val stableUri = doc.uri.toString()
|
||||
metadataObj.put("id", buildStableLibraryId(stableUri))
|
||||
metadataObj.put("filePath", stableUri)
|
||||
metadataObj.put("fileModTime", lastModified)
|
||||
results.put(metadataObj)
|
||||
} catch (_: Exception) {
|
||||
@@ -1680,7 +1690,9 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
} else {
|
||||
try {
|
||||
val safeLastModified = try { doc.lastModified() } catch (_: Exception) { lastModified }
|
||||
metadataObj.put("filePath", doc.uri.toString())
|
||||
val stableUri = doc.uri.toString()
|
||||
metadataObj.put("id", buildStableLibraryId(stableUri))
|
||||
metadataObj.put("filePath", stableUri)
|
||||
metadataObj.put("fileModTime", safeLastModified)
|
||||
metadataObj.put("lastModified", safeLastModified)
|
||||
results.put(metadataObj)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user