recover media records after cloud rename
This commit is contained in:
@@ -108,7 +108,12 @@ class _MediaLibraryPageState extends ConsumerState<MediaLibraryPage> {
|
|||||||
ref.listen<MediaLibraryState>(mediaLibraryProvider, (previous, next) {
|
ref.listen<MediaLibraryState>(mediaLibraryProvider, (previous, next) {
|
||||||
final message = next.errorMessage ?? next.statusMessage;
|
final message = next.errorMessage ?? next.statusMessage;
|
||||||
final previousMessage = previous?.errorMessage ?? previous?.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;
|
return;
|
||||||
}
|
}
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
|||||||
@@ -573,31 +573,15 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
|||||||
final apiKey = StorageManager.get<String>(StorageKeys.tmdbApiKey) ?? '';
|
final apiKey = StorageManager.get<String>(StorageKeys.tmdbApiKey) ?? '';
|
||||||
state = state.copyWith(statusMessage: '正在同步云盘文件信息…', clearError: true);
|
state = state.copyWith(statusMessage: '正在同步云盘文件信息…', clearError: true);
|
||||||
final updates = <MediaLibraryItem>[];
|
final updates = <MediaLibraryItem>[];
|
||||||
|
final replacements = <String, MediaLibraryItem>{};
|
||||||
final failures = <String>[];
|
final failures = <String>[];
|
||||||
var recognizedCount = 0;
|
var recognizedCount = 0;
|
||||||
for (final original in originals) {
|
for (final original in originals) {
|
||||||
try {
|
try {
|
||||||
final detail = await _api!.fsDetail(original.file.id);
|
final latestFile = await _resolveCurrentCloudFile(original.file);
|
||||||
final fromCloud = _fileFromDetail(
|
if (latestFile == null) {
|
||||||
detail,
|
throw StateError('云盘中未找到该文件');
|
||||||
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 fallback = MediaLibraryItem.fromFile(
|
final fallback = MediaLibraryItem.fromFile(
|
||||||
original.libraryID,
|
original.libraryID,
|
||||||
latestFile,
|
latestFile,
|
||||||
@@ -624,16 +608,24 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
|||||||
}
|
}
|
||||||
updated = await _renameMatchedMediaFile(updated);
|
updated = await _renameMatchedMediaFile(updated);
|
||||||
updates.add(updated);
|
updates.add(updated);
|
||||||
|
replacements['${original.libraryID}:${original.id}'] = updated;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
failures.add('${original.file.name}: $error');
|
failures.add('${original.file.name}: $error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (updates.isNotEmpty) {
|
if (updates.isNotEmpty) {
|
||||||
await _store.upsertItems(updates);
|
await _replaceItemsByPreviousIDs(replacements);
|
||||||
_searchResultsCache.clear();
|
_searchResultsCache.clear();
|
||||||
final byID = {for (final item in updates) item.id: item};
|
final byID = {for (final item in updates) item.id: item};
|
||||||
state = state.copyWith(
|
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
|
statusMessage: failures.isEmpty
|
||||||
? '已同步 ${updates.length} 个资源,自动识别 $recognizedCount 个'
|
? '已同步 ${updates.length} 个资源,自动识别 $recognizedCount 个'
|
||||||
: '已同步 ${updates.length} 个资源,识别 $recognizedCount 个,${failures.length} 个同步失败',
|
: '已同步 ${updates.length} 个资源,识别 $recognizedCount 个,${failures.length} 个同步失败',
|
||||||
@@ -911,7 +903,12 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await _api!.fsRename(item.file.id, targetName);
|
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);
|
await _writeNfoForRenamedMedia(updated, parsed);
|
||||||
return updated;
|
return updated;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -1364,6 +1361,107 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<CloudFile?> _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 = <CloudFile>[];
|
||||||
|
final candidateIDs = <String>{};
|
||||||
|
void addCandidates(Iterable<CloudFile> 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<CloudFile> _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(
|
CloudFile? _fileFromDetail(
|
||||||
Map<String, dynamic> detail,
|
Map<String, dynamic> detail,
|
||||||
String fileID,
|
String fileID,
|
||||||
@@ -1524,6 +1622,22 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
|
|||||||
return _store.replaceItems(items);
|
return _store.replaceItems(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _replaceItemsByPreviousIDs(
|
||||||
|
Map<String, MediaLibraryItem> 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<int> _removeDiscInternalItems() async {
|
Future<int> _removeDiscInternalItems() async {
|
||||||
final items = await _loadAllItems();
|
final items = await _loadAllItems();
|
||||||
final retained = items
|
final retained = items
|
||||||
|
|||||||
Reference in New Issue
Block a user