using Microsoft.Extensions.Options; using TeleWave.Domain.Media; using TeleWave.Infrastructure.Media; using Xunit; namespace TeleWave.Integration.Tests; /// /// Файловая часть ручного разбора: перечисление, перенос и подчистка спутников. Работает с реальной /// ФС во временном каталоге — код удаляет файлы, и проверять его на моках бессмысленно. /// public sealed class ManualInboxStorageTests : IDisposable { private readonly string _root = Path.Combine( Path.GetTempPath(), "telewave-tests-" + Guid.NewGuid().ToString("N")[..8] ); private readonly FileSystemMediaStorage _storage; private readonly MediaPathResolver _paths; public ManualInboxStorageTests() { _paths = new MediaPathResolver(Options.Create(new StorageOptions { RootPath = _root })); _paths.EnsureDirectories(); _storage = new FileSystemMediaStorage(_paths); } [Fact] public void ListManualInbox_WalksSubdirectories_AndReturnsRelativePaths() { Write("Сериал/s01e01.mkv"); Write("Сериал/s01e02.mkv"); Write("одиночный.mp4"); var files = _storage.ListManualInbox(int.MaxValue); Assert.Equal(3, files.Count); // Разделитель нормализован: путь уезжает в URL и возвращается строкой запроса. Assert.Contains(files, f => f.RelativePath == "Сериал/s01e01.mkv"); Assert.Contains(files, f => f.RelativePath == "одиночный.mp4"); Assert.All(files, f => Assert.True(f.SizeBytes > 0)); } [Fact] public async Task PromoteToOriginal_MovesFileOutOfManual() { Write("Сериал/s01e01.mkv"); var assetId = Guid.NewGuid(); await _storage.PromoteToOriginalAsync( MediaSource.ManualInbox, "Сериал/s01e01.mkv", assetId, ".mkv", default ); Assert.False(File.Exists(Path.Combine(_paths.ManualDir, "Сериал", "s01e01.mkv"))); Assert.True(File.Exists(_paths.OriginalPath(assetId, ".mkv"))); } [Fact] public async Task Cleanup_RemovesSiblings_ButKeepsOtherVideos() { Write("Сериал/s01e01.mkv"); Write("Сериал/s01e01.srt"); Write("Сериал/s01e01.ru.srt"); Write("Сериал/s01e01.nfo"); Write("Сериал/s01e01.Extended.mkv"); Write("Сериал/s01e02.mkv"); Write("Сериал/s01e02.srt"); // Так делает импорт: сначала файл уходит в originals/, потом подчищаются спутники. await _storage.PromoteToOriginalAsync( MediaSource.ManualInbox, "Сериал/s01e01.mkv", Guid.NewGuid(), ".mkv", default ); await _storage.CleanupManualLeftoversAsync("Сериал/s01e01.mkv", default); var left = Directory .EnumerateFiles(Path.Combine(_paths.ManualDir, "Сериал")) .Select(Path.GetFileName) .ToList(); // Субтитры и nfo забранной серии ушли… Assert.DoesNotContain("s01e01.srt", left); Assert.DoesNotContain("s01e01.ru.srt", left); Assert.DoesNotContain("s01e01.nfo", left); // …а другой материал и спутники соседних серий остались. Assert.Contains("s01e01.Extended.mkv", left); Assert.Contains("s01e02.mkv", left); Assert.Contains("s01e02.srt", left); } [Fact] public async Task Cleanup_RemovesEmptiedFolder_ButNotManualRoot() { Write("Сериал/один.mkv"); Write("Сериал/один.srt"); Write("в-корне.mkv"); await _storage.PromoteToOriginalAsync( MediaSource.ManualInbox, "Сериал/один.mkv", Guid.NewGuid(), ".mkv", default ); await _storage.CleanupManualLeftoversAsync("Сериал/один.mkv", default); Assert.False(Directory.Exists(Path.Combine(_paths.ManualDir, "Сериал"))); // Файл в корне забираем — сам корень manual/ остаться обязан. await _storage.PromoteToOriginalAsync( MediaSource.ManualInbox, "в-корне.mkv", Guid.NewGuid(), ".mkv", default ); await _storage.CleanupManualLeftoversAsync("в-корне.mkv", default); Assert.True(Directory.Exists(_paths.ManualDir)); } [Fact] public async Task Cleanup_RemovesSubtitlesFromNestedFolders() { // Раскладка из жизни: субтитры лежат не рядом с серией, а в подпапках по языкам, и названы // иначе — совпадает только номер серии. Write("Season 16/S16E21.mkv"); Write("Season 16/subs_ru/S16E21 - The Father, the Son.srt"); Write("Season 16/subs_en/S16E21 - The Father, the Son.srt"); Write("Season 16/subs_ru/S16E20 - Home Away From Homer.srt"); Write("Season 16/extras/S16E21 удалённые сцены.mkv"); await _storage.PromoteToOriginalAsync( MediaSource.ManualInbox, "Season 16/S16E21.mkv", Guid.NewGuid(), ".mkv", default ); await _storage.CleanupManualLeftoversAsync("Season 16/S16E21.mkv", default); var season = Path.Combine(_paths.ManualDir, "Season 16"); Assert.False( File.Exists(Path.Combine(season, "subs_ru", "S16E21 - The Father, the Son.srt")) ); Assert.False( File.Exists(Path.Combine(season, "subs_en", "S16E21 - The Father, the Son.srt")) ); // Субтитры соседней серии остаются: её ещё не забирали. Assert.True( File.Exists(Path.Combine(season, "subs_ru", "S16E20 - Home Away From Homer.srt")) ); // Видео не трогаем нигде, даже с тем же номером серии, — это другой материал. Assert.True(File.Exists(Path.Combine(season, "extras", "S16E21 удалённые сцены.mkv"))); } [Fact] public async Task Cleanup_RemovesEmptiedNestedFolders_AfterLastEpisode() { Write("Season 16/S16E01.mkv"); Write("Season 16/S16E02.mkv"); Write("Season 16/subs_ru/S16E01 - Treehouse.srt"); Write("Season 16/subs_ru/S16E02 - All's Fair.srt"); foreach (var episode in new[] { "S16E01", "S16E02" }) { await _storage.PromoteToOriginalAsync( MediaSource.ManualInbox, $"Season 16/{episode}.mkv", Guid.NewGuid(), ".mkv", default ); await _storage.CleanupManualLeftoversAsync($"Season 16/{episode}.mkv", default); } // Опустевшие подпапки уходят вместе с каталогом сезона — корень manual/ остаётся. Assert.False(Directory.Exists(Path.Combine(_paths.ManualDir, "Season 16"))); Assert.True(Directory.Exists(_paths.ManualDir)); } [Fact] public async Task Cleanup_KeepsFolderThatStillHasFiles() { Write("Сериал/s01e01.mkv"); Write("Сериал/s01e02.mkv"); await _storage.PromoteToOriginalAsync( MediaSource.ManualInbox, "Сериал/s01e01.mkv", Guid.NewGuid(), ".mkv", default ); await _storage.CleanupManualLeftoversAsync("Сериал/s01e01.mkv", default); Assert.True(File.Exists(Path.Combine(_paths.ManualDir, "Сериал", "s01e02.mkv"))); } private void Write(string relativePath) { var full = Path.Combine( _paths.ManualDir, relativePath.Replace('/', Path.DirectorySeparatorChar) ); Directory.CreateDirectory(Path.GetDirectoryName(full)!); File.WriteAllText(full, "x"); } public void Dispose() { if (Directory.Exists(_root)) Directory.Delete(_root, recursive: true); } }