-
-
Notifications
You must be signed in to change notification settings - Fork 14
commit 590b791
abduznik edited this page May 23, 2026
·
1 revision
Commit: 590b791ff57c4afe7b63cb081cb45cfab2023b90
Author: abduznik
Date: 2026年04月29日
Why: Restructures existing code without changing external behavior — improves maintainability, readability, or performance.
lib/core/romm/rom_scanner_service.dart | 188 +++++++++++++--------------------
1 file changed, 73 insertions(+), 115 deletions(-)
lib/core/romm/rom_scanner_service.dart
diff --git a/lib/core/romm/rom_scanner_service.dart b/lib/core/romm/rom_scanner_service.dart index 11d3c95..9cbb501 100644 --- a/lib/core/romm/rom_scanner_service.dart +++ b/lib/core/romm/rom_scanner_service.dart @@ -26,9 +26,9 @@ class RomScannerService { RomScannerService(this._rommService, this._mappingService, this._directoryService); /// Performs an incremental sync of the ROM directory. + /// Performs a high-performance sync of the ROM directory. Stream<RomSyncResult> sync(String romsRoot, {bool force = false}) async* { final mappings = _mappingService.getMappings(); - final rootDir = Directory(romsRoot); if (!await rootDir.exists()) return; @@ -38,63 +38,42 @@ class RomScannerService { final List<Directory> platformDirs = []; await for (final entity in rootDir.list()) { - if (entity is Directory) { - platformDirs.add(entity); - } + if (entity is Directory) platformDirs.add(entity); } final storedMTimes = _mappingService.getMTimes(); - final List<Directory> dirtyDirs = []; for (final dir in platformDirs) { - final storedMTime = storedMTimes[dir.path]; - final stat = await dir.stat(); - if (storedMTime != stat.modified.millisecondsSinceEpoch) { - dirtyDirs.add(dir); - } - } - - bool forceFullScan = force || (mappings.length < 10 && platformDirs.length > 5); - - if (!force && dirtyDirs.isEmpty && !forceFullScan) { - debugPrint('[RomScanner] No platform directories changed. Skipping scan.'); - return; - } - - final dirsToScan = forceFullScan ? platformDirs : dirtyDirs; - debugPrint('[RomScanner] Scanning ${dirsToScan.length} directories...'); - - for (final dir in dirsToScan) { final platformSlug = p.basename(dir.path).toLowerCase(); final platformId = platformSlugToId[platformSlug]; + if (platformId == null) continue; + + final stat = await dir.stat(); + final storedMTime = storedMTimes[dir.path]; - if (platformId == null) { - debugPrint('[RomScanner] Skipping unknown platform folder: $platformSlug'); + // If not forced and mtime matches, skip if we already have some mappings for this platform + if (!force && storedMTime == stat.modified.millisecondsSinceEpoch && mappings.values.any((id) => mappings.entries.any((e) => p.isWithin(dir.path, e.key)))) { continue; } - debugPrint('[RomScanner] Processing $platformSlug (ID: $platformId)...'); + debugPrint('[RomScanner] Syncing platform: $platformSlug...'); - // 1. Fetch all games for this platform from RomM + // 1. Fetch ALL games for this platform from RomM final List<Game> platformGames = []; try { int offset = 0; - const int batchSize = 250; + const int batchSize = 500; while (true) { - final result = await _rommService.getGamesPage( - offset: offset, - limit: batchSize, - platformId: platformId - ); + final result = await _rommService.getGamesPage(offset: offset, limit: batchSize, platformId: platformId); platformGames.addAll(result.games); if (platformGames.length >= result.total || result.games.isEmpty) break; offset += batchSize; } } catch (e) { - debugPrint('[RomScanner] Error fetching platform games for $platformSlug: $e'); + debugPrint('[RomScanner] Error fetching platform games: $e'); continue; } - // 2. Build File System Index for this platform + // 2. Build local indices for fast lookup final index = await FileSystemIndex.build(dir.path); final romsSubDir = p.join(dir.path, 'roms'); FileSystemIndex? romsIndex; @@ -103,115 +82,96 @@ class RomScannerService { } final Set<String> mappedPathsInThisDir = {}; - final Set<String> matchedRomIdsInThisDir = {}; + final Set<String> matchedRomIdsInThisPlatform = {}; final Set<String> allGlobalMappedIds = mappings.values.toSet(); - // PASS 1: File-centric matching (Identify existing files) + // --- PASS 1: DIRECT LIBRARY-TO-DISK MATCHING (Locked Matches) --- + // We iterate through the LIBRARY first. This is the most reliable way. + for (final game in platformGames) { + // If already mapped globally, check if file still exists + final existingPath = mappings.entries.where((e) => e.value == game.id).map((e) => e.key).firstOrNull; + if (existingPath != null) { + if (await File(existingPath).exists() || await Directory(existingPath).exists()) { + mappedPathsInThisDir.add(existingPath); + matchedRomIdsInThisPlatform.add(game.id); + continue; + } else { + await _mappingService.removeMapping(existingPath); + allGlobalMappedIds.remove(game.id); + } + } + + // Try to find the game on disk using official fileName/fsName/Name + String? foundPath = await _directoryService.findExistingRomPath(game, index: index); + if (foundPath == null && romsIndex != null) { + foundPath = await _directoryService.findExistingRomPath(game, index: romsIndex); + } + + if (foundPath != null) { + debugPrint('[Scanner] Direct Match: ${game.name} -> $foundPath'); + await _mappingService.updateMapping(foundPath, game.id); + mappedPathsInThisDir.add(foundPath); + matchedRomIdsInThisPlatform.add(game.id); + allGlobalMappedIds.add(game.id); + yield RomSyncResult(foundPath, game.id, game: game); + } + } + + // --- PASS 2: DISK-TO-LIBRARY DISCOVERY (Strict Fallback) --- + // For any files on disk not yet matched, try to find their corresponding game in the platform library. final allLocalEntities = [...index.files.values, ...index.dirs.values]; if (romsIndex != null) { allLocalEntities.addAll([...romsIndex.files.values, ...romsIndex.dirs.values]); } for (final entityPath in allLocalEntities) { - if (mappings.containsKey(entityPath)) { - mappedPathsInThisDir.add(entityPath); - matchedRomIdsInThisDir.add(mappings[entityPath]!); - continue; - } + if (mappedPathsInThisDir.contains(entityPath)) continue; final fileName = p.basename(entityPath); final fileNameNoExt = p.basenameWithoutExtension(entityPath).toLowerCase(); - Game? matchedGame; - - // Strategy A: Exact match against RomM filenames/fsnames - matchedGame = platformGames.cast<Game?>().firstWhere( - (g) => g?.fileName == fileName || g?.fsName == fileName, + // A. Match by exact fileName/fsName + Game? matchedGame = platformGames.cast<Game?>().firstWhere( + (g) => (g?.fileName == fileName || g?.fsName == fileName) && !matchedRomIdsInThisPlatform.contains(g!.id), orElse: () => null, ); - // Strategy B: Clean name match + // B. Match by strict clean name + size if (matchedGame == null) { final fNameClean = _cleanName(fileNameNoExt); - final candidates = platformGames.where((g) { - if (_cleanName(g.name) == fNameClean) return true; - final gFileNoExt = p.basenameWithoutExtension(g.fileName ?? '').toLowerCase(); - if (_cleanName(gFileNoExt) == fNameClean) return true; - final gFsNoExt = p.basenameWithoutExtension(g.fsName ?? '').toLowerCase(); - if (_cleanName(gFsNoExt) == fNameClean) return true; - return false; - }).toList(); - - if (candidates.length == 1) { - final candidate = candidates.first; - // Additional confirmation: If both have sizes, they should be reasonably close - final localSize = index.fileSizes[entityPath] ?? (romsIndex?.fileSizes[entityPath] ?? 0); - if (candidate.fileSize > 0 && localSize > 0) { - // Allow for small differences in size (e.g. metadata or padding) but not completely different - final diff = (candidate.fileSize - localSize).abs(); - if (diff < 1024 * 1024 * 10) { // < 10MB difference - matchedGame = candidate; + if (fNameClean.length > 3) { + final candidates = platformGames.where((g) { + if (matchedRomIdsInThisPlatform.contains(g.id)) return false; + if (_cleanName(g.name) == fNameClean) return true; + final gFileNoExt = p.basenameWithoutExtension(g.fileName ?? '').toLowerCase(); + return _cleanName(gFileNoExt) == fNameClean; + }).toList(); + + if (candidates.length == 1) { + final candidate = candidates.first; + final localSize = index.fileSizes[entityPath] ?? (romsIndex?.fileSizes[entityPath] ?? 0); + // Require size confirmation for fuzzy matches if available + if (candidate.fileSize > 0 && localSize > 0) { + final diff = (candidate.fileSize - localSize).abs(); + if (diff < 1024 * 1024 * 5) matchedGame = candidate; } else { - debugPrint('[Scanner] Fuzzy name match for $fileName -> ${candidate.name} REJECTED due to size mismatch ($localSize vs ${candidate.fileSize})'); + matchedGame = candidate; } - } else { - // One side has no size, we have to trust the unique name match - matchedGame = candidate; } - } else if (candidates.length > 1) { - debugPrint('[Scanner] Ambiguous match for $fileName: ${candidates.length} candidates found. Skipping.'); } } if (matchedGame != null) { - debugPrint('[Scanner] File Match: $fileName -> ${matchedGame.name}'); + debugPrint('[Scanner] Discovery Match: $fileName -> ${matchedGame.name}'); await _mappingService.updateMapping(entityPath, matchedGame.id); mappedPathsInThisDir.add(entityPath); - matchedRomIdsInThisDir.add(matchedGame.id); + matchedRomIdsInThisPlatform.add(matchedGame.id); allGlobalMappedIds.add(matchedGame.id); yield RomSyncResult(entityPath, matchedGame.id, game: matchedGame); - } else { - // Strategy C: File size match (Very strong indicator if in the correct platform folder) - final fileSize = index.fileSizes[entityPath] ?? (romsIndex?.fileSizes[entityPath] ?? 0); - if (fileSize > 1024 * 1024) { // Only for files > 1MB to avoid collisions on small files - matchedGame = platformGames.cast<Game?>().firstWhere( - (g) => g?.fileSize == fileSize, - orElse: () => null, - ); - if (matchedGame != null) { - debugPrint('[Scanner] Size Match: $fileName -> ${matchedGame.name} ($fileSize bytes)'); - await _mappingService.updateMapping(entityPath, matchedGame.id); - mappedPathsInThisDir.add(entityPath); - matchedRomIdsInThisDir.add(matchedGame.id); - allGlobalMappedIds.add(matchedGame.id); - yield RomSyncResult(entityPath, matchedGame.id, game: matchedGame); - } - } } } - // PASS 2: Game-centric matching (Reverse Mapping) - // For any game in library NOT yet matched, try to find it on disk using DirectoryService logic - for (final game in platformGames) { - if (allGlobalMappedIds.contains(game.id)) continue; - - // Use robust DirectoryService logic with pre-built indices - String? foundPath = await _directoryService.findExistingRomPath(game, index: index); - if (foundPath == null && romsIndex != null) { - foundPath = await _directoryService.findExistingRomPath(game, index: romsIndex); - } - - if (foundPath != null) { - debugPrint('[Scanner] REVERSE Match: ${game.name} -> $foundPath'); - await _mappingService.updateMapping(foundPath, game.id); - mappedPathsInThisDir.add(foundPath); - matchedRomIdsInThisDir.add(game.id); - allGlobalMappedIds.add(game.id); - yield RomSyncResult(foundPath, game.id, game: game); - } - } - - // 3. Identify REMOVALS for this platform + // 3. Identify REMOVALS final platformMappings = mappings.entries.where((e) => p.isWithin(dir.path, e.key)); for (final entry in platformMappings) { if (!mappedPathsInThisDir.contains(entry.key) && !await File(entry.key).exists() && !await Directory(entry.key).exists()) { @@ -220,8 +180,6 @@ class RomScannerService { } } - // Update mtime to mark directory as synced - final stat = await dir.stat(); await _mappingService.updateMTime(dir.path, stat.modified.millisecondsSinceEpoch); } }