Skip to content

commit 911c495

abduznik edited this page May 23, 2026 · 1 revision

fix: implement global pruning and metadata auto-repair to resolve UI desync

Commit: 911c495ee4ebffbb92bbd6881e3c7802244a22dc

Author: abduznik

Date: 2026-04-29

Why: Fixes a bug or regression in the existing codebase.

Files Changed

lib/core/romm/rom_scanner_service.dart             | 13 ++++++++
 lib/providers/downloaded_games_cache_provider.dart | 35 ++++++++++++++++++++--
 2 files changed, 46 insertions(+), 2 deletions(-)
  • lib/core/romm/rom_scanner_service.dart
  • lib/providers/downloaded_games_cache_provider.dart

Diff

diff --git a/lib/core/romm/rom_scanner_service.dart b/lib/core/romm/rom_scanner_service.dart
index 13c8532..e0925d7 100644
--- a/lib/core/romm/rom_scanner_service.dart
+++ b/lib/core/romm/rom_scanner_service.dart
@@ -182,4 +182,17 @@ class RomScannerService {
     }
   }
 
+
+  /// Prunes all mappings that point to non-existent files.
+  Future<int> pruneDeadMappings() async {
+    final mappings = _mappingService.getMappings();
+    int count = 0;
+    for (final entry in mappings.entries) {
+      if (!await File(entry.key).exists() && !await Directory(entry.key).exists()) {
+        await _mappingService.removeMapping(entry.key);
+        count++;
+      }
+    }
+    return count;
+  }
 }
diff --git a/lib/providers/downloaded_games_cache_provider.dart b/lib/providers/downloaded_games_cache_provider.dart
index 32dbf1d..9370969 100644
--- a/lib/providers/downloaded_games_cache_provider.dart
+++ b/lib/providers/downloaded_games_cache_provider.dart
@@ -64,12 +64,19 @@ class DownloadedGamesCache extends StateNotifier<Map<String, bool>> {
   /// Also verifies they actually exist on disk to prevent 'zombie' games.
   Future<void> refresh() async {
     final mappingServiceAsync = _ref.read(romMappingServiceProvider);
-    if (!mappingServiceAsync.hasValue) return;
+    final metadataCacheAsync = _ref.read(metadataCacheServiceProvider);
+    final rommService = _ref.read(rommServiceProvider);
+    
+    if (!mappingServiceAsync.hasValue || !metadataCacheAsync.hasValue) return;
     
     final mappingService = mappingServiceAsync.value!;
+    final metadataCache = metadataCacheAsync.value!;
     final mappings = mappingService.getMappings();
     final Map<String, bool> newState = {};
     
+    final Set<String> missingMetadataIds = {};
+    final cachedIds = metadataCache.cachedGames.map((g) => g.id).toSet();
+
     for (final entry in mappings.entries) {
       final path = entry.key;
       final romId = entry.value;
@@ -77,6 +84,9 @@ class DownloadedGamesCache extends StateNotifier<Map<String, bool>> {
       // DISK CHECK: If the file was deleted manually, clean up the mapping
       if (await io.File(path).exists() || await io.Directory(path).exists()) {
         newState[romId] = true;
+        if (!cachedIds.contains(romId)) {
+          missingMetadataIds.add(romId);
+        }
       } else {
         debugPrint('[DownloadedGamesCache] Zombie detected: $path no longer exists. Removing.');
         await mappingService.removeMapping(path);
@@ -84,7 +94,22 @@ class DownloadedGamesCache extends StateNotifier<Map<String, bool>> {
     }
     
     state = newState;
-    debugPrint('[DownloadedGamesCache] Loaded ${state.length} active mappings.');
+    debugPrint('[DownloadedGamesCache] Loaded ${state.length} active mappings (${missingMetadataIds.length} missing metadata).');
+
+    // AUTO-REPAIR: Fetch missing metadata in small batches
+    if (missingMetadataIds.isNotEmpty && rommService != null) {
+      debugPrint('[DownloadedGamesCache] Repairing metadata for ${missingMetadataIds.length} games...');
+      final List<Game> repairedGames = [];
+      for (final id in missingMetadataIds.take(20)) { // Limit to 20 per refresh to avoid spam
+        final game = await rommService.getGame(id);
+        if (game != null) repairedGames.add(game);
+      }
+      if (repairedGames.isNotEmpty) {
+        await metadataCache.saveGames(repairedGames);
+        debugPrint('[DownloadedGamesCache] Repaired ${repairedGames.length} metadata entries.');
+        // Don't call refresh again to avoid loops, the next state update will pick it up
+      }
+    }
   }
 
   /// Runs the high-performance incremental sync.
@@ -106,6 +131,12 @@ class DownloadedGamesCache extends StateNotifier<Map<String, bool>> {
     debugPrint('[DownloadedGamesCache] Starting incremental sync...');
 
     try {
+      // 1. Global Pruning Phase
+      final prunedCount = await scanner.pruneDeadMappings();
+      if (prunedCount > 0) {
+        debugPrint('[DownloadedGamesCache] Pruned $prunedCount dead mappings.');
+      }
+
       final romsRoot = await dirService.getRomsDirectory();
       final List<Game> matchedMetadataBuffer = [];
       final Map<String, bool> sessionMatches = {};

Clone this wiki locally