Skip to content

commit d1d2a77

abduznik edited this page May 23, 2026 · 1 revision

fix(scanner): robust fuzzy matching for filenames with special characters

Commit: d1d2a77dd5dfd4a966a41fc9548c71d37d594a5f

Author: abduznik

Date: 2026-04-28

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

Files Changed

lib/core/romm/rom_scanner_service.dart  | 20 +++++++++++++++++++-
 lib/core/storage/directory_service.dart |  8 ++++----
 2 files changed, 23 insertions(+), 5 deletions(-)
  • lib/core/romm/rom_scanner_service.dart
  • lib/core/storage/directory_service.dart

Diff

diff --git a/lib/core/romm/rom_scanner_service.dart b/lib/core/romm/rom_scanner_service.dart
index a8db14e..6a5063d 100644
--- a/lib/core/romm/rom_scanner_service.dart
+++ b/lib/core/romm/rom_scanner_service.dart
@@ -132,13 +132,31 @@ class RomScannerService {
       
       final results = await Future.wait(chunk.map((filePath) async {
         final fileName = p.basename(filePath);
+        // 1. Try exact filename match first
         try {
-          final searchResult = await _rommService.searchRoms(search: fileName);
+          var searchResult = await _rommService.searchRoms(search: fileName);
           if (searchResult.isNotEmpty) {
             final game = searchResult.first;
             await _mappingService.updateMapping(filePath, game.id);
             return RomSyncResult(filePath, game.id, game: game);
           }
+
+          // 2. FUZZY FALLBACK: Strip extension and clean up symbols
+          // This fixes the issue where 'Game! - (USA).gba' wouldn't match 'Game'
+          final cleanName = p.basenameWithoutExtension(fileName)
+              .replaceAll(RegExp(r'[<>:"/\\|?*!\-\(\)\[\]]'), ' ') // Clean more symbols
+              .replaceAll(RegExp(r'\s+'), ' ')
+              .trim();
+          
+          if (cleanName.isNotEmpty && cleanName != fileName) {
+            debugPrint('[RomScanner] Exact match failed, trying fuzzy search for: $cleanName');
+            searchResult = await _rommService.searchRoms(search: cleanName);
+            if (searchResult.isNotEmpty) {
+              final game = searchResult.first;
+              await _mappingService.updateMapping(filePath, game.id);
+              return RomSyncResult(filePath, game.id, game: game);
+            }
+          }
         } catch (e) {
           debugPrint('[RomScanner] Error matching $fileName: $e');
         }
diff --git a/lib/core/storage/directory_service.dart b/lib/core/storage/directory_service.dart
index c75554c..dd3fc2f 100644
--- a/lib/core/storage/directory_service.dart
+++ b/lib/core/storage/directory_service.dart
@@ -406,7 +406,7 @@ class DirectoryService {
   /// Returns the found path or null if not found.
   Future<String?> findExistingRomPath(Game game) async {
     final romDir = await getRomDirectory(game);
-    final baseName = game.fsName ?? game.fileName ?? game.name.replaceAll(RegExp(r'[<>:"/\\|?*]'), ' ').replaceAll(RegExp(r'\s+'), ' ').trim();
+    final baseName = game.fsName ?? game.fileName ?? game.name.replaceAll(RegExp(r'[<>:"/\\|?*!\-\(\)\[\]]'), ' ').replaceAll(RegExp(r'\s+'), ' ').trim();
     
     // 1. Check exact path first (file or directory) in primary platform folder
     final exactPath = p.join(romDir, baseName);
@@ -420,7 +420,7 @@ class DirectoryService {
     }
 
     // 3. Search for multi-file folder (sanitized game name)
-    final folderName = game.name.replaceAll(RegExp(r'[<>:"/\\|?*]'), ' ').replaceAll(RegExp(r'\s+'), ' ').trim();
+    final folderName = game.name.replaceAll(RegExp(r'[<>:"/\\|?*!\-\(\)\[\]]'), ' ').replaceAll(RegExp(r'\s+'), ' ').trim();
     
     // Try both romDir and romDir/roms
     final searchDirs = [romDir, p.join(romDir, 'roms')];
@@ -441,7 +441,7 @@ class DirectoryService {
         await for (final entity in parentDir.list()) {
           if (entity is Directory) {
             final dName = p.basename(entity.path);
-            final sanitizedDName = dName.replaceAll(RegExp(r'[<>:"/\\|?*]'), ' ').replaceAll(RegExp(r'\s+'), ' ').trim();
+            final sanitizedDName = dName.replaceAll(RegExp(r'[<>:"/\\|?*!\-\(\)\[\]]'), ' ').replaceAll(RegExp(r'\s+'), ' ').trim();
             if (sanitizedDName.toLowerCase() == folderName.toLowerCase()) {
               final found = await _findMainRomInFolder(game, entity.path);
               if (found != null) return found;
@@ -473,7 +473,7 @@ class DirectoryService {
         await for (final entity in parentDir.list()) {
           if (entity is File) {
             final fname = p.basename(entity.path);
-            final sanitizedFName = fname.replaceAll(RegExp(r'[<>:"/\\|?*]'), ' ').replaceAll(RegExp(r'\s+'), ' ').trim();
+            final sanitizedFName = fname.replaceAll(RegExp(r'[<>:"/\\|?*!\-\(\)\[\]]'), ' ').replaceAll(RegExp(r'\s+'), ' ').trim();
             if (sanitizedFName.toLowerCase().startsWith(baseName.toLowerCase()) && !fname.toLowerCase().endsWith('.part')) {
               // Prioritize .nds for NDS
               if ((game.platformSlug?.toLowerCase() == 'nds' || game.platformSlug?.toLowerCase() == 'nintendo-ds') && fname.toLowerCase().endsWith('.nds')) {

Clone this wiki locally