From f8ee960c92533260f65c4604a657cf8859e7e3e2 Mon Sep 17 00:00:00 2001 From: ngfchl Date: Sat, 18 Jul 2026 20:33:54 +0800 Subject: [PATCH] skip optical disc streams in media scans --- lib/providers/media_library_provider.dart | 53 ++++++++++++++++++++++- test/media_scan_filter_test.dart | 34 +++++++++++++++ 2 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 test/media_scan_filter_test.dart diff --git a/lib/providers/media_library_provider.dart b/lib/providers/media_library_provider.dart index 222746c..42cbd6b 100644 --- a/lib/providers/media_library_provider.dart +++ b/lib/providers/media_library_provider.dart @@ -10,6 +10,23 @@ import '../core/storage/storage_manager.dart'; import '../models/cloud_file.dart'; import '../models/media_library.dart'; +/// Blu-ray and DVD folders contain transport streams rather than standalone +/// media. They must be handled as a disc structure, never as individual files. +bool isMediaScanDiscInternalPath(String path) { + return path + .split(RegExp(r'[/\\]+')) + .map((component) => component.trim().toUpperCase()) + .any((component) => component == 'BDMV' || component == 'VIDEO_TS'); +} + +bool isMediaScanDiscLayout(Iterable children) { + final directoryNames = children + .where((file) => file.isDirectory) + .map((file) => file.name.trim().toUpperCase()) + .toSet(); + return directoryNames.contains('BDMV') || directoryNames.contains('VIDEO_TS'); +} + class MediaLibraryState { final List libraries; final String? selectedLibraryID; @@ -107,6 +124,7 @@ class MediaLibraryNotifier extends StateNotifier { try { await _store.initialize(); await _migrateLegacyHiveIfNeeded(); + final removedDiscStreams = await _removeDiscInternalItems(); final libraries = await _loadLibraries(); final selectedID = libraries.isEmpty ? null : libraries.first.id; final items = selectedID == null @@ -118,6 +136,9 @@ class MediaLibraryNotifier extends StateNotifier { selectedLibraryID: selectedID, items: items, scanLogs: logs, + statusMessage: removedDiscStreams == 0 + ? null + : '已清理 $removedDiscStreams 个光盘目录内部文件', ); if (selectedID != null) { unawaited(_hydrateMissingArtwork(selectedID, items)); @@ -297,6 +318,10 @@ class MediaLibraryNotifier extends StateNotifier { ); try { + final removedDiscStreams = await _removeDiscInternalItems(); + if (removedDiscStreams > 0) { + _appendScanLog('已清理 $removedDiscStreams 个已入库的光盘目录内部文件'); + } final initialItems = await _loadAllItems() ..removeWhere((item) => item.libraryID == library.id); final unique = { @@ -1053,6 +1078,13 @@ class MediaLibraryNotifier extends StateNotifier { final folder = folders.removeAt(0); final visitKey = folder.id ?? 'root'; if (!visited.add(visitKey)) continue; + // A library source can point at BDMV/VIDEO_TS directly. There is no + // parent disc folder to inspect in that case, so skip it before listing + // and scraping its internal transport streams. + if (isMediaScanDiscInternalPath(folder.path)) { + _appendScanLog('跳过光盘目录内部文件:${folder.path}'); + continue; + } var page = 0; final cachedFolder = await FileMetadataCache.folderChildren(folder.id); @@ -1073,11 +1105,18 @@ class MediaLibraryNotifier extends StateNotifier { files = await _enrichAndCacheFiles(files); folderSnapshot.addAll(files); } + // A folder containing BDMV or VIDEO_TS is a disc root. Do not descend + // into any of its children: BDMV/STREAM and VIDEO_TS files are segments + // of one work, not separately scrapeable media files. + final isDiscRoot = isMediaScanDiscLayout(files); final mediaBatch = []; for (final file in files) { if (file.isDirectory) { - if (recursive) { - folders.add(_ScanFolder(file.id, '${folder.path}/${file.name}')); + if (recursive && !isDiscRoot) { + final childPath = '${folder.path}/${file.name}'; + if (!isMediaScanDiscInternalPath(childPath)) { + folders.add(_ScanFolder(file.id, childPath)); + } } } else if (file.isVideo && (file.size ?? 0) >= minimumSizeBytes) { mediaBatch.add(_withPath(file, '${folder.path}/${file.name}')); @@ -1403,6 +1442,16 @@ class MediaLibraryNotifier extends StateNotifier { return _store.replaceItems(items); } + Future _removeDiscInternalItems() async { + final items = await _loadAllItems(); + final retained = items + .where((item) => !isMediaScanDiscInternalPath(item.file.cloudPath)) + .toList(); + final removed = items.length - retained.length; + if (removed > 0) await _saveAllItems(retained); + return removed; + } + Future _upsertItems(Iterable items) { return _store.upsertItems(items); } diff --git a/test/media_scan_filter_test.dart b/test/media_scan_filter_test.dart new file mode 100644 index 0000000..0119299 --- /dev/null +++ b/test/media_scan_filter_test.dart @@ -0,0 +1,34 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:guangya_flutter/models/cloud_file.dart'; +import 'package:guangya_flutter/providers/media_library_provider.dart'; + +CloudFile directory(String name) => + CloudFile(id: name, name: name, isDirectory: true); + +void main() { + group('media scan optical-disc filtering', () { + test('identifies Blu-ray and DVD internal paths', () { + expect( + isMediaScanDiscInternalPath('/Movies/Example/BDMV/STREAM/00000.m2ts'), + isTrue, + ); + expect( + isMediaScanDiscInternalPath(r'Movies\\Example\\VIDEO_TS\\VTS_01_1.VOB'), + isTrue, + ); + expect( + isMediaScanDiscInternalPath('/Movies/Example/Feature/01.m2ts'), + isFalse, + ); + }); + + test('recognizes folders that are optical-disc roots', () { + expect(isMediaScanDiscLayout([directory('BDMV')]), isTrue); + expect(isMediaScanDiscLayout([directory('video_ts')]), isTrue); + expect( + isMediaScanDiscLayout([directory('Feature'), directory('Extras')]), + isFalse, + ); + }); + }); +}