sync media cache after file renames
This commit is contained in:
@@ -314,7 +314,17 @@ class _FileSearchResultsPageState extends ConsumerState<FileSearchResultsPage> {
|
|||||||
onSelect: () => _selectFile(files, file),
|
onSelect: () => _selectFile(files, file),
|
||||||
onOpen: () => notifier.downloadFile(file),
|
onOpen: () => notifier.downloadFile(file),
|
||||||
onRenameConfirm: (name) async {
|
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());
|
if (mounted) setState(() => _results = _search());
|
||||||
},
|
},
|
||||||
onCopy: () => notifier.copyToClipboard([file]),
|
onCopy: () => notifier.copyToClipboard([file]),
|
||||||
|
|||||||
@@ -1407,7 +1407,14 @@ class _PrimaryFilePane extends ConsumerWidget {
|
|||||||
onOpen: file.isDirectory
|
onOpen: file.isDirectory
|
||||||
? () => notifier.navigateToFolder(file)
|
? () => notifier.navigateToFolder(file)
|
||||||
: () => _openCloudFile(context, ref, 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]),
|
onCopy: () => notifier.copyToClipboard([file]),
|
||||||
onCut: () => notifier.cutToClipboard([file]),
|
onCut: () => notifier.cutToClipboard([file]),
|
||||||
onDownload: () => notifier.downloadFile(file),
|
onDownload: () => notifier.downloadFile(file),
|
||||||
@@ -2066,6 +2073,9 @@ class _ColumnFileBrowserState extends ConsumerState<_ColumnFileBrowser> {
|
|||||||
controller.dispose();
|
controller.dispose();
|
||||||
if (newName == null || newName.isEmpty || newName == file.name) return;
|
if (newName == null || newName.isEmpty || newName == file.name) return;
|
||||||
await ref.read(authProvider.notifier).api.fsRename(file.id, newName);
|
await ref.read(authProvider.notifier).api.fsRename(file.id, newName);
|
||||||
|
await ref.read(mediaLibraryProvider.notifier).synchronizeRenamedFiles([
|
||||||
|
file.copyWith(name: newName),
|
||||||
|
]);
|
||||||
final affected = <int>{};
|
final affected = <int>{};
|
||||||
for (var index = 0; index < _columns.length; index++) {
|
for (var index = 0; index < _columns.length; index++) {
|
||||||
if (_columns[index].files.any((item) => item.id == file.id)) {
|
if (_columns[index].files.any((item) => item.id == file.id)) {
|
||||||
@@ -3086,7 +3096,14 @@ class _SecondaryFilePaneState extends ConsumerState<_SecondaryFilePane> {
|
|||||||
onCut: () =>
|
onCut: () =>
|
||||||
ref.read(fileProvider.notifier).cutToClipboard([file]),
|
ref.read(fileProvider.notifier).cutToClipboard([file]),
|
||||||
onRenameConfirm: (name) async {
|
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;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_files = _files
|
_files = _files
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import '../models/fast_transfer.dart';
|
|||||||
import '../models/media_library.dart';
|
import '../models/media_library.dart';
|
||||||
import '../providers/auth_provider.dart';
|
import '../providers/auth_provider.dart';
|
||||||
import '../providers/file_provider.dart';
|
import '../providers/file_provider.dart';
|
||||||
|
import '../providers/media_library_provider.dart';
|
||||||
import 'media_library_page.dart';
|
import 'media_library_page.dart';
|
||||||
|
|
||||||
enum WorkspaceTool { scan, rename, fastTransfer, tmdb, categories }
|
enum WorkspaceTool { scan, rename, fastTransfer, tmdb, categories }
|
||||||
@@ -985,6 +986,7 @@ class _BatchRenameToolState extends ConsumerState<_BatchRenameTool> {
|
|||||||
final api = ref.read(authProvider.notifier).api;
|
final api = ref.read(authProvider.notifier).api;
|
||||||
var succeeded = 0;
|
var succeeded = 0;
|
||||||
var failed = 0;
|
var failed = 0;
|
||||||
|
final renamedFiles = <CloudFile>[];
|
||||||
try {
|
try {
|
||||||
for (final change in changes) {
|
for (final change in changes) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -992,12 +994,18 @@ class _BatchRenameToolState extends ConsumerState<_BatchRenameTool> {
|
|||||||
try {
|
try {
|
||||||
await api.fsRename(change.file.id, change.newName);
|
await api.fsRename(change.file.id, change.newName);
|
||||||
succeeded += 1;
|
succeeded += 1;
|
||||||
|
renamedFiles.add(change.file.copyWith(name: change.newName));
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
failed += 1;
|
failed += 1;
|
||||||
}
|
}
|
||||||
if (mounted) setState(() => _completed += 1);
|
if (mounted) setState(() => _completed += 1);
|
||||||
}
|
}
|
||||||
await ref.read(fileProvider.notifier).loadFiles();
|
await ref.read(fileProvider.notifier).loadFiles();
|
||||||
|
if (renamedFiles.isNotEmpty) {
|
||||||
|
await ref
|
||||||
|
.read(mediaLibraryProvider.notifier)
|
||||||
|
.synchronizeRenamedFiles(renamedFiles);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|||||||
@@ -625,19 +625,23 @@ class FileNotifier extends StateNotifier<FileState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> renameFile(CloudFile file, String newName) async {
|
Future<bool> renameFile(CloudFile file, String newName) async {
|
||||||
if (_api == null) return;
|
if (_api == null) return false;
|
||||||
try {
|
try {
|
||||||
await _api!.fsRename(file.id, newName);
|
await _api!.fsRename(file.id, newName);
|
||||||
|
final renamed = file.copyWith(name: newName);
|
||||||
state = state.copyWith(statusMessage: '重命名成功');
|
state = state.copyWith(statusMessage: '重命名成功');
|
||||||
await FileMetadataCache.updateFolderChildren(
|
await FileMetadataCache.updateFolderChildren(
|
||||||
_currentParentID,
|
_currentParentID,
|
||||||
addOrReplace: [file.copyWith(name: newName)],
|
addOrReplace: [renamed],
|
||||||
);
|
);
|
||||||
|
await FileMetadataCache.cacheFiles([renamed]);
|
||||||
await _invalidateListCache(_currentParentID);
|
await _invalidateListCache(_currentParentID);
|
||||||
await loadFiles(parentID: _currentParentID);
|
await loadFiles(parentID: _currentParentID);
|
||||||
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
state = state.copyWith(errorMessage: e.toString());
|
state = state.copyWith(errorMessage: e.toString());
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -576,10 +576,18 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
|||||||
final failures = <String>[];
|
final failures = <String>[];
|
||||||
for (final original in originals) {
|
for (final original in originals) {
|
||||||
try {
|
try {
|
||||||
|
_appendScanLog(
|
||||||
|
'[同步识别][DEBUG] 开始:${original.file.name} '
|
||||||
|
'(fileId=${original.id}, gcid=${original.file.gcid ?? '-'})',
|
||||||
|
);
|
||||||
final latestFile = await _resolveCurrentCloudFile(original.file);
|
final latestFile = await _resolveCurrentCloudFile(original.file);
|
||||||
if (latestFile == null) {
|
if (latestFile == null) {
|
||||||
throw StateError('云盘中未找到该文件');
|
throw StateError('云盘中未找到该文件');
|
||||||
}
|
}
|
||||||
|
_appendScanLog(
|
||||||
|
'[同步识别][DEBUG] 云盘已同步:fileId ${original.id} -> '
|
||||||
|
'${latestFile.id},名称=${latestFile.name},gcid=${latestFile.gcid ?? '-'}',
|
||||||
|
);
|
||||||
synced.add(
|
synced.add(
|
||||||
_SyncedMediaItem(
|
_SyncedMediaItem(
|
||||||
original: original,
|
original: original,
|
||||||
@@ -592,6 +600,10 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
failures.add('${original.file.name}: $error');
|
failures.add('${original.file.name}: $error');
|
||||||
|
_appendScanLog(
|
||||||
|
'[同步识别][DEBUG] 同步失败:${original.file.name},$error',
|
||||||
|
isError: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (synced.isEmpty) {
|
if (synced.isEmpty) {
|
||||||
@@ -613,6 +625,15 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
|||||||
final original = entry.original;
|
final original = entry.original;
|
||||||
try {
|
try {
|
||||||
final fallback = entry.fallback;
|
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
|
var updated = apiKey.trim().isEmpty
|
||||||
? fallback
|
? fallback
|
||||||
: await _recognizeMediaItem(
|
: await _recognizeMediaItem(
|
||||||
@@ -623,7 +644,18 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
|||||||
proxyPort:
|
proxyPort:
|
||||||
StorageManager.get<String>(StorageKeys.tmdbProxyPort) ?? '',
|
StorageManager.get<String>(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
|
// A temporary TMDB or network failure must not erase a previously
|
||||||
// scraped item just because its refreshed file metadata was available.
|
// scraped item just because its refreshed file metadata was available.
|
||||||
if (updated.tmdbID == null && original.tmdbID != null) {
|
if (updated.tmdbID == null && original.tmdbID != null) {
|
||||||
@@ -634,11 +666,22 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
|||||||
}
|
}
|
||||||
final beforeName = updated.file.name;
|
final beforeName = updated.file.name;
|
||||||
updated = await _renameMatchedMediaFile(updated);
|
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);
|
updates.add(updated);
|
||||||
replacements['${original.libraryID}:${original.id}'] = updated;
|
replacements['${original.libraryID}:${original.id}'] = updated;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
failures.add('${original.file.name}: $error');
|
failures.add('${original.file.name}: $error');
|
||||||
|
_appendScanLog(
|
||||||
|
'[同步识别][DEBUG] 识别失败:${original.file.name},$error',
|
||||||
|
isError: true,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (updates.isNotEmpty) {
|
if (updates.isNotEmpty) {
|
||||||
@@ -664,6 +707,63 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Keeps SQLite media records aligned with file-manager rename operations
|
||||||
|
/// without re-scraping or overriding a user-initiated name change.
|
||||||
|
Future<void> synchronizeRenamedFiles(Iterable<CloudFile> 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 = <String, MediaLibraryItem>{};
|
||||||
|
|
||||||
|
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<MediaLibraryItem> applyTMDBMatch(
|
Future<MediaLibraryItem> applyTMDBMatch(
|
||||||
MediaLibraryItem item,
|
MediaLibraryItem item,
|
||||||
Map<String, dynamic> candidate,
|
Map<String, dynamic> candidate,
|
||||||
@@ -1020,6 +1120,11 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
|||||||
return index <= 0 ? '' : cloudPath.substring(0, index);
|
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) {
|
String _normalizeMediaTitle(String value) {
|
||||||
return value.toLowerCase().replaceAll(
|
return value.toLowerCase().replaceAll(
|
||||||
RegExp(r'[^a-z0-9\u4e00-\u9fff]+'),
|
RegExp(r'[^a-z0-9\u4e00-\u9fff]+'),
|
||||||
@@ -1403,7 +1508,7 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
|||||||
final candidateIDs = <String>{};
|
final candidateIDs = <String>{};
|
||||||
void addCandidates(Iterable<CloudFile> files) {
|
void addCandidates(Iterable<CloudFile> files) {
|
||||||
for (final file in files) {
|
for (final file in files) {
|
||||||
if (!file.isDirectory &&
|
if (file.isDirectory == knownFile.isDirectory &&
|
||||||
file.name == knownFile.name &&
|
file.name == knownFile.name &&
|
||||||
candidateIDs.add(file.id)) {
|
candidateIDs.add(file.id)) {
|
||||||
candidates.add(file);
|
candidates.add(file);
|
||||||
|
|||||||
Reference in New Issue
Block a user