use global cloud index for media rescans

This commit is contained in:
ngfchl
2026-07-18 21:59:05 +08:00
parent c4f3383795
commit 58e324ba22
+124 -23
View File
@@ -333,6 +333,13 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
_appendScanLog('已清理 $removedDiscStreams 个已入库的光盘目录内部文件');
}
final previousLibraryItems = await _loadItems(library.id);
final existingByID = {
for (final item in previousLibraryItems) item.id: item,
};
final existingByGCID = {
for (final item in previousLibraryItems)
if (item.file.gcid?.isNotEmpty == true) item.file.gcid!: item,
};
final initialItems = await _loadAllItems()
..removeWhere((item) => item.libraryID == library.id);
final unique = <String, MediaLibraryItem>{
@@ -361,13 +368,18 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
file,
directoryName: _parentDirectoryName(file.cloudPath),
);
final existing = unique[file.id];
final fileChanged =
final existing =
unique[file.id] ??
existingByID[file.id] ??
(file.gcid?.isNotEmpty == true
? existingByGCID[file.gcid!]
: null);
final sameCloudResource =
existing != null &&
(existing.file.name != file.name ||
existing.file.gcid != file.gcid ||
existing.file.cloudPath != file.cloudPath);
var item = existing == null || fileChanged
(existing.id == file.id ||
(file.gcid?.isNotEmpty == true &&
file.gcid == existing.file.gcid));
var item = existing == null || !sameCloudResource
? await _recognizeMediaItem(
fallback,
tmdbApiKey,
@@ -416,23 +428,46 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
await Future.wait(List.generate(concurrency, (_) => worker()));
}
for (final source in library.sources) {
if (_cancelScan) break;
_appendScanLog('扫描目录:${source.path}');
state = state.copyWith(
progress: MediaLibraryScanProgress(
phase: '扫描 ${source.path}',
completed: completed,
),
);
await _scanSource(
source.rootID,
source.path,
recursive: library.recursive,
minimumSizeBytes: library.minimumSizeMB * 1024 * 1024,
forceRemote: forceRemote,
onMediaFiles: indexBatch,
);
if (forceRemote) {
_appendScanLog('正在获取云盘全量文件索引…');
final globalFiles = await _allGlobalRemoteFiles();
final seen = <String>{};
final mediaFiles = <CloudFile>[];
for (final source in library.sources) {
for (final file in globalFiles) {
if (!file.isVideo ||
(file.size ?? 0) < library.minimumSizeMB * 1024 * 1024 ||
!_isWithinLibrarySource(file, source) ||
!seen.add(file.id)) {
continue;
}
final path = _globalCloudPath(file, globalFiles, source.path);
if (!isMediaScanDiscInternalPath(path)) {
mediaFiles.add(_withPath(file, path));
}
}
}
_appendScanLog('全量索引完成,发现 ${mediaFiles.length} 个媒体文件');
await indexBatch(mediaFiles);
} else {
for (final source in library.sources) {
if (_cancelScan) break;
_appendScanLog('扫描目录:${source.path}');
state = state.copyWith(
progress: MediaLibraryScanProgress(
phase: '扫描 ${source.path}',
completed: completed,
),
);
await _scanSource(
source.rootID,
source.path,
recursive: library.recursive,
minimumSizeBytes: library.minimumSizeMB * 1024 * 1024,
forceRemote: false,
onMediaFiles: indexBatch,
);
}
}
await pendingPersistence;
var items = unique.values.toList()
@@ -1446,6 +1481,72 @@ class MediaLibraryNotifier extends StateNotifier<MediaLibraryState> {
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB';
}
Future<List<CloudFile>> _allGlobalRemoteFiles() async {
final batches = await Future.wait([
_allGlobalRemoteFilesByType(),
_allGlobalRemoteFilesByType(resType: 2),
]);
final unique = {
for (final file in batches.expand((files) => files)) file.id: file,
};
final values = unique.values.toList();
await FileMetadataCache.cacheFiles(values);
return values;
}
Future<List<CloudFile>> _allGlobalRemoteFilesByType({int? resType}) async {
const pageSize = 10000;
final values = <CloudFile>[];
for (var page = 0; !_cancelScan; page++) {
final response = await _api!.fsFiles(
parentID: '*',
page: page,
pageSize: pageSize,
orderBy: 0,
sortType: 0,
resType: resType,
);
final batch = _extractFiles(response);
values.addAll(batch);
final total = _findIntDeep(response, const [
'total',
'totalCount',
'count',
]);
if (batch.length < pageSize ||
(total != null && values.length >= total)) {
break;
}
}
return values;
}
bool _isWithinLibrarySource(CloudFile file, MediaLibrarySource source) {
final rootID = source.rootID;
if (rootID == null || rootID.isEmpty) return true;
if (file.parentID == rootID || file.id == rootID) return true;
return (file.fullParentIDs ?? '')
.split('/')
.where((id) => id.isNotEmpty)
.contains(rootID);
}
String _globalCloudPath(
CloudFile file,
Iterable<CloudFile> allFiles,
String sourcePath,
) {
final namesByID = {for (final item in allFiles) item.id: item.name};
final parents = (file.fullParentIDs ?? '')
.split('/')
.where((id) => id.isNotEmpty)
.map((id) => namesByID[id])
.whereType<String>()
.toList();
if (parents.isEmpty) return '$sourcePath/${file.name}';
return '/${[...parents, file.name].join('/')}';
}
Future<void> _scanSource(
String? rootID,
String rootPath, {