From 2f89f62149ebbc3f3bb98eb730fda843bb772022 Mon Sep 17 00:00:00 2001 From: ngfchl Date: Sat, 18 Jul 2026 20:50:55 +0800 Subject: [PATCH] recover media records after cloud rename --- lib/pages/media_library_page.dart | 7 +- lib/providers/media_library_provider.dart | 162 ++++++++++++++++++---- 2 files changed, 144 insertions(+), 25 deletions(-) diff --git a/lib/pages/media_library_page.dart b/lib/pages/media_library_page.dart index 6dec7d9..ed4dec9 100644 --- a/lib/pages/media_library_page.dart +++ b/lib/pages/media_library_page.dart @@ -108,7 +108,12 @@ class _MediaLibraryPageState extends ConsumerState { ref.listen(mediaLibraryProvider, (previous, next) { final message = next.errorMessage ?? next.statusMessage; final previousMessage = previous?.errorMessage ?? previous?.statusMessage; - if (message == null || message.isEmpty || message == previousMessage) { + final isProgressMessage = + next.errorMessage == null && message?.startsWith('正在') == true; + if (message == null || + message.isEmpty || + message == previousMessage || + isProgressMessage) { return; } WidgetsBinding.instance.addPostFrameCallback((_) { diff --git a/lib/providers/media_library_provider.dart b/lib/providers/media_library_provider.dart index 0587737..2e2bdef 100644 --- a/lib/providers/media_library_provider.dart +++ b/lib/providers/media_library_provider.dart @@ -573,31 +573,15 @@ class MediaLibraryNotifier extends StateNotifier { final apiKey = StorageManager.get(StorageKeys.tmdbApiKey) ?? ''; state = state.copyWith(statusMessage: '正在同步云盘文件信息…', clearError: true); final updates = []; + final replacements = {}; final failures = []; var recognizedCount = 0; for (final original in originals) { try { - final detail = await _api!.fsDetail(original.file.id); - final fromCloud = _fileFromDetail( - detail, - original.file.id, - original.file, - ); - final latestName = fromCloud?.name ?? original.file.name; - final latestGCID = fromCloud?.gcid ?? original.file.gcid; - final parentPath = _parentPath(original.file.cloudPath); - final latestFile = original.file.copyWith( - name: latestName, - size: fromCloud?.size, - gcid: latestGCID, - modifiedAt: fromCloud?.modifiedAt, - cloudPath: parentPath.isEmpty - ? latestName - : '$parentPath/$latestName', - parentID: fromCloud?.parentID, - fullParentIDs: fromCloud?.fullParentIDs, - ); - await FileMetadataCache.cacheFiles([latestFile]); + final latestFile = await _resolveCurrentCloudFile(original.file); + if (latestFile == null) { + throw StateError('云盘中未找到该文件'); + } final fallback = MediaLibraryItem.fromFile( original.libraryID, latestFile, @@ -624,16 +608,24 @@ class MediaLibraryNotifier extends StateNotifier { } updated = await _renameMatchedMediaFile(updated); updates.add(updated); + replacements['${original.libraryID}:${original.id}'] = updated; } catch (error) { failures.add('${original.file.name}: $error'); } } if (updates.isNotEmpty) { - await _store.upsertItems(updates); + await _replaceItemsByPreviousIDs(replacements); _searchResultsCache.clear(); final byID = {for (final item in updates) item.id: item}; state = state.copyWith( - items: state.items.map((item) => byID[item.id] ?? item).toList(), + items: state.items + .map( + (item) => + replacements['${item.libraryID}:${item.id}'] ?? + byID[item.id] ?? + item, + ) + .toList(), statusMessage: failures.isEmpty ? '已同步 ${updates.length} 个资源,自动识别 $recognizedCount 个' : '已同步 ${updates.length} 个资源,识别 $recognizedCount 个,${failures.length} 个同步失败', @@ -911,7 +903,12 @@ class MediaLibraryNotifier extends StateNotifier { try { await _api!.fsRename(item.file.id, targetName); - final updated = item.copyWith(file: item.file.copyWith(name: targetName)); + final renamedFile = + await _resolveCurrentCloudFile( + item.file.copyWith(name: targetName), + ) ?? + item.file.copyWith(name: targetName); + final updated = item.copyWith(file: renamedFile); await _writeNfoForRenamedMedia(updated, parsed); return updated; } catch (_) { @@ -1364,6 +1361,107 @@ class MediaLibraryNotifier extends StateNotifier { ); } + Future _resolveCurrentCloudFile(CloudFile knownFile) async { + try { + final detail = await _api!.fsDetail(knownFile.id); + final current = _fileFromDetail(detail, knownFile.id, knownFile); + if (current != null) { + return _cacheResolvedCloudFile(knownFile, current); + } + } catch (_) { + // A rename can replace the cloud record. Locate its new ID below. + } + + final cached = await FileMetadataCache.file(knownFile.id); + final parentID = knownFile.parentID ?? cached?.parentID; + final candidates = []; + final candidateIDs = {}; + void addCandidates(Iterable files) { + for (final file in files) { + if (!file.isDirectory && + file.name == knownFile.name && + candidateIDs.add(file.id)) { + candidates.add(file); + } + } + } + + if (parentID != null && parentID.isNotEmpty) { + try { + final response = await _api!.fsFiles( + parentID: parentID, + page: 0, + pageSize: 1000, + orderBy: 0, + sortType: 0, + ); + addCandidates(_extractFiles(response)); + } catch (_) { + // The global search below remains available when the parent is stale. + } + } + try { + final response = await _api!.searchFiles( + knownFile.name, + page: 0, + pageSize: 100, + ); + addCandidates(_extractFiles(response)); + } catch (_) { + // Return null below so callers can surface an actionable sync failure. + } + + for (final candidate in candidates) { + var resolved = candidate; + if ((knownFile.gcid?.isNotEmpty ?? false) && + candidate.gcid != knownFile.gcid) { + try { + final detail = await _api!.fsDetail(candidate.id); + resolved = + _fileFromDetail(detail, candidate.id, candidate) ?? candidate; + } catch (_) { + continue; + } + } + if (knownFile.gcid?.isNotEmpty == true && + resolved.gcid != knownFile.gcid) { + continue; + } + return _cacheResolvedCloudFile(knownFile, resolved); + } + return null; + } + + Future _cacheResolvedCloudFile( + CloudFile knownFile, + CloudFile resolved, + ) async { + final parentPath = _parentPath(knownFile.cloudPath); + final file = knownFile.copyWith( + id: resolved.id, + name: resolved.name, + size: resolved.size, + gcid: resolved.gcid, + modifiedAt: resolved.modifiedAt, + cloudPath: parentPath.isEmpty + ? resolved.name + : '$parentPath/${resolved.name}', + parentID: resolved.parentID ?? knownFile.parentID, + fullParentIDs: resolved.fullParentIDs ?? knownFile.fullParentIDs, + fileType: resolved.fileType, + ); + await FileMetadataCache.cacheFiles([file]); + final parentID = file.parentID ?? knownFile.parentID; + if (parentID != null && parentID.isNotEmpty) { + await FileMetadataCache.updateFolderChildren( + parentID, + removeIDs: file.id == knownFile.id ? const [] : [knownFile.id], + addOrReplace: [file], + ); + } + return file; + } + CloudFile? _fileFromDetail( Map detail, String fileID, @@ -1524,6 +1622,22 @@ class MediaLibraryNotifier extends StateNotifier { return _store.replaceItems(items); } + Future _replaceItemsByPreviousIDs( + Map replacements, + ) async { + if (replacements.isEmpty) return; + final allItems = await _loadAllItems(); + final replacementIDs = replacements.values + .map((item) => '${item.libraryID}:${item.id}') + .toSet(); + allItems.removeWhere((item) { + final key = '${item.libraryID}:${item.id}'; + return replacements.containsKey(key) || replacementIDs.contains(key); + }); + allItems.addAll(replacements.values); + await _saveAllItems(allItems); + } + Future _removeDiscInternalItems() async { final items = await _loadAllItems(); final retained = items