diff --git a/lib/providers/media_library_provider.dart b/lib/providers/media_library_provider.dart index 9f40825..0c9f41b 100644 --- a/lib/providers/media_library_provider.dart +++ b/lib/providers/media_library_provider.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:collection'; import 'dart:io'; import 'package:flutter_riverpod/legacy.dart'; @@ -587,7 +588,10 @@ class MediaLibraryNotifier extends StateNotifier { '[同步识别][DEBUG] 开始:${original.file.name} ' '(fileId=${original.id}, gcid=${original.file.gcid ?? '-'})', ); - final latestFile = await _resolveCurrentCloudFile(original.file); + final latestFile = await _resolveCurrentCloudFile( + original.file, + libraryID: original.libraryID, + ); if (latestFile == null) { throw StateError('云盘中未找到该文件'); } @@ -775,7 +779,9 @@ class MediaLibraryNotifier extends StateNotifier { parentID: renamedFile.parentID, fullParentIDs: renamedFile.fullParentIDs, ); - final resolved = await _resolveCurrentCloudFile(known) ?? known; + final resolved = + await _resolveCurrentCloudFile(known, libraryID: item.libraryID) ?? + known; replacements['${item.libraryID}:${item.id}'] = item.copyWith( file: resolved, updatedAt: DateTime.now(), @@ -1618,7 +1624,10 @@ class MediaLibraryNotifier extends StateNotifier { ); } - Future _resolveCurrentCloudFile(CloudFile knownFile) async { + Future _resolveCurrentCloudFile( + CloudFile knownFile, { + String? libraryID, + }) async { try { final detail = await _api!.fsDetail(knownFile.id); final current = _fileFromDetail(detail, knownFile.id, knownFile); @@ -1634,6 +1643,21 @@ class MediaLibraryNotifier extends StateNotifier { final cached = await FileMetadataCache.file(knownFile.id); final parentID = knownFile.parentID ?? cached?.parentID; + final gcid = knownFile.gcid?.trim(); + if (cached != null && + gcid != null && + gcid.isNotEmpty && + cached.gcid == gcid && + cached.id != knownFile.id) { + _appendScanLog( + '[同步识别][DEBUG] 从 GCID 缓存恢复新文件:${knownFile.id} -> ${cached.id}', + ); + return _cacheResolvedCloudFile( + knownFile, + cached, + cloudPath: cached.cloudPath.isEmpty ? null : cached.cloudPath, + ); + } final candidates = []; final candidateIDs = {}; void addCandidates(Iterable files) { @@ -1693,7 +1717,6 @@ class MediaLibraryNotifier extends StateNotifier { // The user may have renamed the file outside this app, so its old name is // no longer searchable. Resolve the previous parent directory by name and // compare every child by GCID, which remains stable across a rename. - final gcid = knownFile.gcid?.trim(); final parentName = _parentDirectoryName(knownFile.cloudPath); if (gcid != null && gcid.isNotEmpty && parentName != null) { try { @@ -1740,13 +1763,167 @@ class MediaLibraryNotifier extends StateNotifier { // The caller emits the final diagnostic after all resolution paths fail. } } + if (gcid != null && gcid.isNotEmpty && parentID != null) { + final located = await _resolveFromCurrentDirectory( + knownFile, + parentID: parentID, + ); + if (located != null) return located; + } + final located = await _resolveFromLibrarySources( + knownFile, + libraryID: libraryID, + ); + if (located != null) return located; return null; } + Future _resolveFromCurrentDirectory( + CloudFile knownFile, { + required String parentID, + }) async { + final gcid = knownFile.gcid?.trim(); + if (gcid == null || gcid.isEmpty) return null; + const maxFolders = 1200; + final parentPath = _parentPath(knownFile.cloudPath); + final queue = Queue<_ScanFolder>()..add(_ScanFolder(parentID, parentPath)); + final visited = {}; + var searchedFolders = 0; + _appendScanLog('[同步识别][DEBUG] 从当前文件目录扫描:parentId=$parentID,gcid=$gcid'); + while (queue.isNotEmpty && searchedFolders < maxFolders) { + final folder = queue.removeFirst(); + if (folder.id == null || !visited.add(folder.id!)) continue; + searchedFolders++; + List children; + try { + children = await _allRemoteFolderFiles(folder.id); + } catch (_) { + continue; + } + await FileMetadataCache.cacheFolderChildren(folder.id, children); + for (final child in children) { + final childPath = '${folder.path}/${child.name}'; + if (child.isDirectory) { + queue.add(_ScanFolder(child.id, childPath)); + continue; + } + var resolved = child; + if (resolved.gcid != gcid) { + try { + final detail = await _api!.fsDetail(child.id); + resolved = _fileFromDetail(detail, child.id, child) ?? child; + } catch (_) { + continue; + } + } + if (resolved.gcid == gcid) { + final exact = _withPath(resolved, childPath); + _appendScanLog( + '[同步识别][DEBUG] 当前目录定位成功:${knownFile.id} -> ' + '${exact.id},路径=${exact.cloudPath}', + ); + return _cacheResolvedCloudFile( + knownFile, + exact, + cloudPath: exact.cloudPath, + ); + } + } + } + _appendScanLog('[同步识别][DEBUG] 当前文件目录未命中:已检查 $searchedFolders 个目录'); + return null; + } + + Future _resolveFromLibrarySources( + CloudFile knownFile, { + String? libraryID, + }) async { + final gcid = knownFile.gcid?.trim(); + if (gcid == null || gcid.isEmpty) return null; + final library = state.libraries + .where((item) => item.id == libraryID) + .firstOrNull; + if (library == null) return null; + + const maxFolders = 6000; + final queue = Queue<_ScanFolder>(); + for (final source in library.sources) { + queue.add(_ScanFolder(source.rootID, source.path)); + } + final visited = {}; + var searchedFolders = 0; + _appendScanLog('[同步识别][DEBUG] 启动媒体库根目录 GCID 定向扫描:$gcid'); + while (queue.isNotEmpty && searchedFolders < maxFolders) { + final folder = queue.removeFirst(); + final key = folder.id ?? 'root:${folder.path}'; + if (!visited.add(key)) continue; + searchedFolders++; + List children; + try { + children = await _allRemoteFolderFiles(folder.id); + } catch (_) { + continue; + } + await FileMetadataCache.cacheFolderChildren(folder.id, children); + for (final child in children) { + final childPath = '${folder.path}/${child.name}'; + if (child.isDirectory) { + queue.add(_ScanFolder(child.id, childPath)); + continue; + } + var resolved = child; + if (resolved.gcid != gcid) { + try { + final detail = await _api!.fsDetail(child.id); + resolved = _fileFromDetail(detail, child.id, child) ?? child; + } catch (_) { + continue; + } + } + if (resolved.gcid == gcid) { + final exact = _withPath(resolved, childPath); + _appendScanLog( + '[同步识别][DEBUG] 媒体库源目录定位成功:${knownFile.id} -> ' + '${exact.id},路径=${exact.cloudPath}', + ); + return _cacheResolvedCloudFile( + knownFile, + exact, + cloudPath: exact.cloudPath, + ); + } + } + } + _appendScanLog( + '[同步识别][DEBUG] 媒体库源目录扫描未命中:已检查 $searchedFolders 个目录', + isError: true, + ); + return null; + } + + Future> _allRemoteFolderFiles(String? parentID) async { + const pageSize = 1000; + final files = []; + for (var page = 0; ; page++) { + final response = await _api!.fsFiles( + parentID: parentID, + page: page, + pageSize: pageSize, + orderBy: 0, + sortType: 0, + ); + final batch = _extractFiles(response); + files.addAll(batch); + if (batch.length < pageSize) break; + } + return files; + } + Future _cacheResolvedCloudFile( CloudFile knownFile, - CloudFile resolved, - ) async { + CloudFile resolved, { + String? cloudPath, + }) async { final parentPath = _parentPath(knownFile.cloudPath); final file = knownFile.copyWith( id: resolved.id, @@ -1754,9 +1931,9 @@ class MediaLibraryNotifier extends StateNotifier { size: resolved.size, gcid: resolved.gcid, modifiedAt: resolved.modifiedAt, - cloudPath: parentPath.isEmpty - ? resolved.name - : '$parentPath/${resolved.name}', + cloudPath: + cloudPath ?? + (parentPath.isEmpty ? resolved.name : '$parentPath/${resolved.name}'), parentID: resolved.parentID ?? knownFile.parentID, fullParentIDs: resolved.fullParentIDs ?? knownFile.fullParentIDs, fileType: resolved.fileType,