using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Errors; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Tests.Fakes; using FluentAssertions; using LanguageExt; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; using static LanguageExt.Prelude; namespace ErsatzTV.Core.Tests.Metadata { [TestFixture] public class MovieFolderScannerTests { private static readonly string BadFakeRoot = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"C:\Movies-That-Dont-Exist" : @"/movies-that-dont-exist"; private static readonly string FakeRoot = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"C:\Movies" : "/movies"; private static readonly string FFprobePath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"C:\bin\ffprobe.exe" : "/bin/ffprobe"; [TestFixture] public class ScanFolder { [SetUp] public void SetUp() { _movieRepository = new Mock(); _movieRepository.Setup(x => x.GetOrAdd(It.IsAny(), It.IsAny())) .Returns( (int _, string path) => Right(new MovieMediaItem { Path = path }).AsTask()); _localStatisticsProvider = new Mock(); _localMetadataProvider = new Mock(); _imageCache = new Mock(); _imageCache.Setup( x => x.ResizeAndSaveImage(FakeLocalFileSystem.TestBytes, It.IsAny(), It.IsAny())) .Returns(Right("poster").AsTask()); } private Mock _movieRepository; private Mock _localStatisticsProvider; private Mock _localMetadataProvider; private Mock _imageCache; [Test] public async Task Missing_Folder() { MovieFolderScanner service = GetService( new FakeFileEntry(Path.Combine(FakeRoot, Path.Combine("Movie (2020)", "Movie (2020).mkv"))) ); var source = new LocalMediaSource { Folder = BadFakeRoot }; Either result = await service.ScanFolder(source, FFprobePath); result.IsLeft.Should().BeTrue(); result.IfLeft(error => error.Should().BeOfType()); } [Test] public async Task NewMovie_Statistics_And_FallbackMetadata( [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] string videoExtension) { string moviePath = Path.Combine( FakeRoot, Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); MovieFolderScanner service = GetService( new FakeFileEntry(moviePath) ); var source = new LocalMediaSource { Id = 1, Folder = FakeRoot }; Either result = await service.ScanFolder(source, FFprobePath); result.IsRight.Should().BeTrue(); _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); _movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once); _localStatisticsProvider.Verify( x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), Times.Once); _localMetadataProvider.Verify( x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), Times.Once); } [Test] public async Task NewMovie_Statistics_And_SidecarMetadata_MovieNameNfo( [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] string videoExtension) { string moviePath = Path.Combine( FakeRoot, Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); string metadataPath = Path.ChangeExtension(moviePath, "nfo"); MovieFolderScanner service = GetService( new FakeFileEntry(moviePath), new FakeFileEntry(metadataPath) ); var source = new LocalMediaSource { Id = 1, Folder = FakeRoot }; Either result = await service.ScanFolder(source, FFprobePath); result.IsRight.Should().BeTrue(); _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); _movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once); _localStatisticsProvider.Verify( x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), Times.Once); _localMetadataProvider.Verify( x => x.RefreshSidecarMetadata(It.Is(i => i.Path == moviePath), metadataPath), Times.Once); } [Test] public async Task NewMovie_Statistics_And_SidecarMetadata_MovieNfo( [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] string videoExtension) { string moviePath = Path.Combine( FakeRoot, Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); string metadataPath = Path.Combine(Path.GetDirectoryName(moviePath) ?? string.Empty, "movie.nfo"); MovieFolderScanner service = GetService( new FakeFileEntry(moviePath), new FakeFileEntry(metadataPath) ); var source = new LocalMediaSource { Id = 1, Folder = FakeRoot }; Either result = await service.ScanFolder(source, FFprobePath); result.IsRight.Should().BeTrue(); _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); _movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once); _localStatisticsProvider.Verify( x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), Times.Once); _localMetadataProvider.Verify( x => x.RefreshSidecarMetadata(It.Is(i => i.Path == moviePath), metadataPath), Times.Once); } [Test] public async Task NewMovie_Statistics_And_FallbackMetadata_And_Poster( [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] string videoExtension, [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ImageFileExtensions))] string imageExtension) { string moviePath = Path.Combine( FakeRoot, Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); string posterPath = Path.Combine( Path.GetDirectoryName(moviePath) ?? string.Empty, $"poster.{imageExtension}"); MovieFolderScanner service = GetService( new FakeFileEntry(moviePath), new FakeFileEntry(posterPath) ); var source = new LocalMediaSource { Id = 1, Folder = FakeRoot }; Either result = await service.ScanFolder(source, FFprobePath); result.IsRight.Should().BeTrue(); _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); _movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once); _localStatisticsProvider.Verify( x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), Times.Once); _localMetadataProvider.Verify( x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), Times.Once); _imageCache.Verify( x => x.ResizeAndSaveImage(FakeLocalFileSystem.TestBytes, It.IsAny(), It.IsAny()), Times.Once); } [Test] public async Task NewMovie_Statistics_And_FallbackMetadata_And_MovieNamePoster( [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] string videoExtension, [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ImageFileExtensions))] string imageExtension) { string moviePath = Path.Combine( FakeRoot, Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); string posterPath = Path.Combine( Path.GetDirectoryName(moviePath) ?? string.Empty, $"Movie (2020)-poster.{imageExtension}"); MovieFolderScanner service = GetService( new FakeFileEntry(moviePath), new FakeFileEntry(posterPath) ); var source = new LocalMediaSource { Id = 1, Folder = FakeRoot }; Either result = await service.ScanFolder(source, FFprobePath); result.IsRight.Should().BeTrue(); _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); _movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once); _localStatisticsProvider.Verify( x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), Times.Once); _localMetadataProvider.Verify( x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), Times.Once); _imageCache.Verify( x => x.ResizeAndSaveImage(FakeLocalFileSystem.TestBytes, It.IsAny(), It.IsAny()), Times.Once); } [Test] public async Task Should_Ignore_Extra_Files( [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] string videoExtension, [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ExtraFiles))] string extraFile) { string moviePath = Path.Combine( FakeRoot, Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); MovieFolderScanner service = GetService( new FakeFileEntry(moviePath), new FakeFileEntry( Path.Combine( Path.GetDirectoryName(moviePath) ?? string.Empty, $"Movie (2020)-{extraFile}{videoExtension}")) ); var source = new LocalMediaSource { Id = 1, Folder = FakeRoot }; Either result = await service.ScanFolder(source, FFprobePath); result.IsRight.Should().BeTrue(); _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); _movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once); _localStatisticsProvider.Verify( x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), Times.Once); _localMetadataProvider.Verify( x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), Times.Once); } [Test] public async Task Should_Ignore_Extra_Folders( [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] string videoExtension, [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.ExtraDirectories))] string extraFolder) { string moviePath = Path.Combine( FakeRoot, Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); MovieFolderScanner service = GetService( new FakeFileEntry(moviePath), new FakeFileEntry( Path.Combine( Path.GetDirectoryName(moviePath) ?? string.Empty, Path.Combine(extraFolder, $"Movie (2020){videoExtension}"))) ); var source = new LocalMediaSource { Id = 1, Folder = FakeRoot }; Either result = await service.ScanFolder(source, FFprobePath); result.IsRight.Should().BeTrue(); _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); _movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once); _localStatisticsProvider.Verify( x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), Times.Once); _localMetadataProvider.Verify( x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), Times.Once); } [Test] public async Task Should_Work_With_Nested_Folders( [ValueSource(typeof(LocalFolderScanner), nameof(LocalFolderScanner.VideoFileExtensions))] string videoExtension) { string moviePath = Path.Combine( Path.Combine(FakeRoot, "L-P"), Path.Combine("Movie (2020)", $"Movie (2020){videoExtension}")); MovieFolderScanner service = GetService( new FakeFileEntry(moviePath) ); var source = new LocalMediaSource { Id = 1, Folder = FakeRoot }; Either result = await service.ScanFolder(source, FFprobePath); result.IsRight.Should().BeTrue(); _movieRepository.Verify(x => x.GetOrAdd(It.IsAny(), It.IsAny()), Times.Once); _movieRepository.Verify(x => x.GetOrAdd(1, moviePath), Times.Once); _localStatisticsProvider.Verify( x => x.RefreshStatistics(FFprobePath, It.Is(i => i.Path == moviePath)), Times.Once); _localMetadataProvider.Verify( x => x.RefreshFallbackMetadata(It.Is(i => i.Path == moviePath)), Times.Once); } private MovieFolderScanner GetService(params FakeFileEntry[] files) => // var mockImageCache = new Mock(); // mockImageCache.Setup(i => i.ResizeAndSaveImage(It.IsAny(), It.IsAny(), It.IsAny())) // .Returns(Right("image").AsTask()); new( new FakeLocalFileSystem(new List(files)), _movieRepository.Object, _localStatisticsProvider.Object, _localMetadataProvider.Object, _imageCache.Object, new Mock>().Object ); } } }