From a3a9ab9c8ddf508dede95cb5f4c2fcd439a41eea Mon Sep 17 00:00:00 2001 From: ngfchl Date: Sat, 18 Jul 2026 21:03:03 +0800 Subject: [PATCH] sync media cache after file renames --- lib/pages/search_results_page.dart | 12 ++- lib/pages/workspace_page.dart | 21 +++- lib/pages/workspace_tools_page.dart | 8 ++ lib/providers/file_provider.dart | 10 +- lib/providers/media_library_provider.dart | 111 +++++++++++++++++++++- 5 files changed, 153 insertions(+), 9 deletions(-) diff --git a/lib/pages/search_results_page.dart b/lib/pages/search_results_page.dart index 6f707f9..a8971f7 100644 --- a/lib/pages/search_results_page.dart +++ b/lib/pages/search_results_page.dart @@ -314,7 +314,17 @@ class _FileSearchResultsPageState extends ConsumerState { onSelect: () => _selectFile(files, file), onOpen: () => notifier.downloadFile(file), onRenameConfirm: (name) async { - await notifier.renameFile(file, name); + final renamed = await notifier.renameFile( + file, + name, + ); + if (renamed) { + await ref + .read(mediaLibraryProvider.notifier) + .synchronizeRenamedFiles([ + file.copyWith(name: name), + ]); + } if (mounted) setState(() => _results = _search()); }, onCopy: () => notifier.copyToClipboard([file]), diff --git a/lib/pages/workspace_page.dart b/lib/pages/workspace_page.dart index 22897ff..c6c6446 100644 --- a/lib/pages/workspace_page.dart +++ b/lib/pages/workspace_page.dart @@ -1407,7 +1407,14 @@ class _PrimaryFilePane extends ConsumerWidget { onOpen: file.isDirectory ? () => notifier.navigateToFolder(file) : () => _openCloudFile(context, ref, file), - onRenameConfirm: (name) => notifier.renameFile(file, name), + onRenameConfirm: (name) async { + final renamed = await notifier.renameFile(file, name); + if (renamed) { + await ref + .read(mediaLibraryProvider.notifier) + .synchronizeRenamedFiles([file.copyWith(name: name)]); + } + }, onCopy: () => notifier.copyToClipboard([file]), onCut: () => notifier.cutToClipboard([file]), onDownload: () => notifier.downloadFile(file), @@ -2066,6 +2073,9 @@ class _ColumnFileBrowserState extends ConsumerState<_ColumnFileBrowser> { controller.dispose(); if (newName == null || newName.isEmpty || newName == file.name) return; await ref.read(authProvider.notifier).api.fsRename(file.id, newName); + await ref.read(mediaLibraryProvider.notifier).synchronizeRenamedFiles([ + file.copyWith(name: newName), + ]); final affected = {}; for (var index = 0; index < _columns.length; index++) { if (_columns[index].files.any((item) => item.id == file.id)) { @@ -3086,7 +3096,14 @@ class _SecondaryFilePaneState extends ConsumerState<_SecondaryFilePane> { onCut: () => ref.read(fileProvider.notifier).cutToClipboard([file]), onRenameConfirm: (name) async { - await ref.read(fileProvider.notifier).renameFile(file, name); + final renamed = await ref + .read(fileProvider.notifier) + .renameFile(file, name); + if (renamed) { + await ref + .read(mediaLibraryProvider.notifier) + .synchronizeRenamedFiles([file.copyWith(name: name)]); + } if (!mounted) return; setState(() { _files = _files diff --git a/lib/pages/workspace_tools_page.dart b/lib/pages/workspace_tools_page.dart index a69432a..b97e0e6 100644 --- a/lib/pages/workspace_tools_page.dart +++ b/lib/pages/workspace_tools_page.dart @@ -14,6 +14,7 @@ import '../models/fast_transfer.dart'; import '../models/media_library.dart'; import '../providers/auth_provider.dart'; import '../providers/file_provider.dart'; +import '../providers/media_library_provider.dart'; import 'media_library_page.dart'; enum WorkspaceTool { scan, rename, fastTransfer, tmdb, categories } @@ -985,6 +986,7 @@ class _BatchRenameToolState extends ConsumerState<_BatchRenameTool> { final api = ref.read(authProvider.notifier).api; var succeeded = 0; var failed = 0; + final renamedFiles = []; try { for (final change in changes) { if (!mounted) return; @@ -992,12 +994,18 @@ class _BatchRenameToolState extends ConsumerState<_BatchRenameTool> { try { await api.fsRename(change.file.id, change.newName); succeeded += 1; + renamedFiles.add(change.file.copyWith(name: change.newName)); } catch (_) { failed += 1; } if (mounted) setState(() => _completed += 1); } await ref.read(fileProvider.notifier).loadFiles(); + if (renamedFiles.isNotEmpty) { + await ref + .read(mediaLibraryProvider.notifier) + .synchronizeRenamedFiles(renamedFiles); + } } finally { if (mounted) { setState(() { diff --git a/lib/providers/file_provider.dart b/lib/providers/file_provider.dart index eedd718..b846d4d 100644 --- a/lib/providers/file_provider.dart +++ b/lib/providers/file_provider.dart @@ -625,19 +625,23 @@ class FileNotifier extends StateNotifier { } } - Future renameFile(CloudFile file, String newName) async { - if (_api == null) return; + Future renameFile(CloudFile file, String newName) async { + if (_api == null) return false; try { await _api!.fsRename(file.id, newName); + final renamed = file.copyWith(name: newName); state = state.copyWith(statusMessage: '重命名成功'); await FileMetadataCache.updateFolderChildren( _currentParentID, - addOrReplace: [file.copyWith(name: newName)], + addOrReplace: [renamed], ); + await FileMetadataCache.cacheFiles([renamed]); await _invalidateListCache(_currentParentID); await loadFiles(parentID: _currentParentID); + return true; } catch (e) { state = state.copyWith(errorMessage: e.toString()); + return false; } } diff --git a/lib/providers/media_library_provider.dart b/lib/providers/media_library_provider.dart index 7ed917f..f9e621d 100644 --- a/lib/providers/media_library_provider.dart +++ b/lib/providers/media_library_provider.dart @@ -576,10 +576,18 @@ class MediaLibraryNotifier extends StateNotifier { final failures = []; for (final original in originals) { try { + _appendScanLog( + '[同步识别][DEBUG] 开始:${original.file.name} ' + '(fileId=${original.id}, gcid=${original.file.gcid ?? '-'})', + ); final latestFile = await _resolveCurrentCloudFile(original.file); if (latestFile == null) { throw StateError('云盘中未找到该文件'); } + _appendScanLog( + '[同步识别][DEBUG] 云盘已同步:fileId ${original.id} -> ' + '${latestFile.id},名称=${latestFile.name},gcid=${latestFile.gcid ?? '-'}', + ); synced.add( _SyncedMediaItem( original: original, @@ -592,6 +600,10 @@ class MediaLibraryNotifier extends StateNotifier { ); } catch (error) { failures.add('${original.file.name}: $error'); + _appendScanLog( + '[同步识别][DEBUG] 同步失败:${original.file.name},$error', + isError: true, + ); } } if (synced.isEmpty) { @@ -613,6 +625,15 @@ class MediaLibraryNotifier extends StateNotifier { final original = entry.original; try { final fallback = entry.fallback; + final parsed = ParsedMediaName.parse( + fallback.file.name, + directoryName: _parentDirectoryName(fallback.file.cloudPath), + ); + _appendScanLog( + '[同步识别][DEBUG] 解析:${fallback.file.name} -> ' + '标题=${parsed.title},年份=${parsed.year ?? '-'},' + '类型=${fallback.mediaKind?.name ?? 'automatic'}', + ); var updated = apiKey.trim().isEmpty ? fallback : await _recognizeMediaItem( @@ -623,7 +644,18 @@ class MediaLibraryNotifier extends StateNotifier { proxyPort: StorageManager.get(StorageKeys.tmdbProxyPort) ?? '', ); - if (updated.tmdbID != null) recognizedCount++; + if (updated.tmdbID != null) { + recognizedCount++; + _appendScanLog( + '[同步识别][DEBUG] TMDB 命中:${updated.title} ' + '(tmdbId=${updated.tmdbID}, ${updated.year.isEmpty ? '年份未知' : updated.year})', + ); + } else { + _appendScanLog( + '[同步识别][DEBUG] TMDB 未命中:${parsed.title} ' + '(年份=${parsed.year ?? '-'})', + ); + } // A temporary TMDB or network failure must not erase a previously // scraped item just because its refreshed file metadata was available. if (updated.tmdbID == null && original.tmdbID != null) { @@ -634,11 +666,22 @@ class MediaLibraryNotifier extends StateNotifier { } final beforeName = updated.file.name; updated = await _renameMatchedMediaFile(updated); - if (updated.file.name != beforeName) renamedCount++; + if (updated.file.name != beforeName) { + renamedCount++; + _appendScanLog( + '[同步识别][DEBUG] 已规范命名:$beforeName -> ${updated.file.name}', + ); + } else if (updated.tmdbID != null) { + _appendScanLog('[同步识别][DEBUG] 跳过规范命名:未解析到分辨率或名称已符合规则'); + } updates.add(updated); replacements['${original.libraryID}:${original.id}'] = updated; } catch (error) { failures.add('${original.file.name}: $error'); + _appendScanLog( + '[同步识别][DEBUG] 识别失败:${original.file.name},$error', + isError: true, + ); } } if (updates.isNotEmpty) { @@ -664,6 +707,63 @@ class MediaLibraryNotifier extends StateNotifier { } } + /// Keeps SQLite media records aligned with file-manager rename operations + /// without re-scraping or overriding a user-initiated name change. + Future synchronizeRenamedFiles(Iterable values) async { + if (_api == null) return; + final renamed = {for (final file in values) file.id: file}.values.toList(); + if (renamed.isEmpty) return; + final allItems = await _loadAllItems(); + final replacements = {}; + + for (final renamedFile in renamed) { + final oldPath = renamedFile.cloudPath; + final newPath = _cloudPathWithName(oldPath, renamedFile.name); + if (renamedFile.isDirectory) { + for (final item in allItems) { + final path = item.file.cloudPath; + if (path != oldPath && !path.startsWith('$oldPath/')) continue; + final suffix = path.substring(oldPath.length); + final updated = item.copyWith( + file: item.file.copyWith(cloudPath: '$newPath$suffix'), + updatedAt: DateTime.now(), + ); + replacements['${item.libraryID}:${item.id}'] = updated; + } + final resolved = await _resolveCurrentCloudFile(renamedFile); + if (resolved != null && resolved.id != renamedFile.id) { + await FileMetadataCache.updateFolderChildren( + renamedFile.id, + invalidate: true, + ); + } + continue; + } + + for (final item in allItems.where((item) => item.id == renamedFile.id)) { + final known = item.file.copyWith( + name: renamedFile.name, + cloudPath: newPath, + parentID: renamedFile.parentID, + fullParentIDs: renamedFile.fullParentIDs, + ); + final resolved = await _resolveCurrentCloudFile(known) ?? known; + replacements['${item.libraryID}:${item.id}'] = item.copyWith( + file: resolved, + updatedAt: DateTime.now(), + ); + } + } + if (replacements.isEmpty) return; + await _replaceItemsByPreviousIDs(replacements); + _searchResultsCache.clear(); + state = state.copyWith( + items: state.items + .map((item) => replacements['${item.libraryID}:${item.id}'] ?? item) + .toList(), + ); + } + Future applyTMDBMatch( MediaLibraryItem item, Map candidate, @@ -1020,6 +1120,11 @@ class MediaLibraryNotifier extends StateNotifier { return index <= 0 ? '' : cloudPath.substring(0, index); } + String _cloudPathWithName(String cloudPath, String name) { + final parentPath = _parentPath(cloudPath); + return parentPath.isEmpty ? name : '$parentPath/$name'; + } + String _normalizeMediaTitle(String value) { return value.toLowerCase().replaceAll( RegExp(r'[^a-z0-9\u4e00-\u9fff]+'), @@ -1403,7 +1508,7 @@ class MediaLibraryNotifier extends StateNotifier { final candidateIDs = {}; void addCandidates(Iterable files) { for (final file in files) { - if (!file.isDirectory && + if (file.isDirectory == knownFile.isDirectory && file.name == knownFile.name && candidateIDs.add(file.id)) { candidates.add(file);