using System.IO; using System.Linq; namespace EmbyToolbox.Services; public sealed class FileDiscoveryService { /// Стабильная сортировка списка видеофайлов по полному нормализованному пути (без учёта регистра). public static StringComparer QueuePathOrderComparer { get; } = StringComparer.OrdinalIgnoreCase; /// /// Собирает пути к поддерживаемым видео: отдельные файлы, из каталогов — рекурсивно, без дублей. /// public IReadOnlyList CollectVideoFilesFromFileSystemEntries(IEnumerable? entryPaths, Action? onError = null) { if (entryPaths is null) { return []; } var set = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var entry in entryPaths) { if (string.IsNullOrWhiteSpace(entry)) { continue; } var full = Path.GetFullPath(entry); if (File.Exists(full)) { if (SupportedVideoFormats.IsSupportedVideoFile(full)) { set.Add(full); } continue; } if (Directory.Exists(full)) { foreach (var file in DiscoverVideoFiles(full, onError)) { set.Add(file); } } } return SortVideoPathsByFullPath(set); } /// Возвращает новый список путей, отсортированный по (стабильно). public static IReadOnlyList SortVideoPathsByFullPath(IEnumerable paths) => paths.OrderBy(p => p, QueuePathOrderComparer).ToList(); public IReadOnlyList DiscoverVideoFiles(string rootDirectory, Action? onError = null) { if (string.IsNullOrWhiteSpace(rootDirectory) || !Directory.Exists(rootDirectory)) { return []; } var result = new List(); var pending = new Stack(); pending.Push(rootDirectory); while (pending.Count > 0) { var current = pending.Pop(); try { foreach (var file in Directory.EnumerateFiles(current)) { if (!SupportedVideoFormats.IsSupportedVideoFile(file)) { continue; } string normalized; try { normalized = Path.GetFullPath(file); } catch { continue; } result.Add(normalized); } } catch (Exception ex) { onError?.Invoke($"ошибка чтения каталога '{current}': {ex.Message}"); } try { foreach (var childDirectory in Directory.EnumerateDirectories(current)) { pending.Push(childDirectory); } } catch (Exception ex) { onError?.Invoke($"ошибка чтения вложенных каталогов '{current}': {ex.Message}"); } } return SortVideoPathsByFullPath(result); } public bool IsSupportedVideoFile(string path) => SupportedVideoFormats.IsSupportedVideoFile(path); }