using System.IO.Abstractions; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Jellyfin; using ErsatzTV.Core.Metadata; using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.Metadata; using ErsatzTV.Scanner.Core.Metadata; using Microsoft.Extensions.Logging; using NSubstitute; using NUnit.Framework; using Shouldly; namespace ErsatzTV.Scanner.Tests.Core.Metadata; // #477: a successful-but-empty media-server fetch must not flag a non-empty movie library FileNotFound // (which EmptyTrash could then permanently delete). Assert the sweep is skipped when nothing came in. public class MediaServerMovieLibraryScannerTests { [TestFixture] public class CleanupFileNotFoundItems { [Test] public async Task Empty_Incoming_With_Existing_Movies_Does_Not_Flag() { var movieRepository = Substitute.For(); var scannerProxy = Substitute.For(); var library = new JellyfinLibrary { Id = 3, Name = "Movies" }; movieRepository.GetExistingMovies(library) .Returns(new List { new() { ItemId = "movie-1", State = MediaItemState.Normal }, new() { ItemId = "movie-2", State = MediaItemState.Normal } }); var scanner = new TestMovieLibraryScanner(scannerProxy); Either result = await scanner.Scan( movieRepository, new JellyfinConnectionParameters("http://jellyfin.example", "abc", 7), library); result.IsRight.ShouldBeTrue(); await movieRepository.DidNotReceive().FlagFileNotFound( Arg.Any(), Arg.Any>()); await scannerProxy.DidNotReceive().ReindexMediaItems( Arg.Any(), Arg.Any()); } } // Minimal concrete subclass exposing the cleanup path. Driven with an empty incoming list, so none of // the per-item metadata members below are ever invoked — they exist only to satisfy the contract. private sealed class TestMovieLibraryScanner : MediaServerMovieLibraryScanner< JellyfinConnectionParameters, JellyfinLibrary, JellyfinMovie, JellyfinItemEtag> { public TestMovieLibraryScanner(IScannerProxy scannerProxy) : base( scannerProxy, Substitute.For(), Substitute.For(), Substitute.For(), Substitute.For()) { } public Task> Scan( IJellyfinMovieRepository movieRepository, JellyfinConnectionParameters connectionParameters, JellyfinLibrary library) => ScanLibrary( movieRepository, connectionParameters, library, _ => string.Empty, false, CancellationToken.None); protected override IAsyncEnumerable> GetMovieLibraryItems( JellyfinConnectionParameters connectionParameters, JellyfinLibrary library) => EmptyMovies(); protected override string MediaServerItemId(JellyfinMovie movie) => movie.ItemId; protected override string MediaServerEtag(JellyfinMovie movie) => movie.Etag; protected override Task> GetFullMetadata( JellyfinConnectionParameters connectionParameters, JellyfinLibrary library, MediaItemScanResult result, JellyfinMovie incoming, bool deepScan) => throw new NotSupportedException(); protected override Task>> GetFullMetadataAndStatistics( JellyfinConnectionParameters connectionParameters, JellyfinLibrary library, MediaItemScanResult result, JellyfinMovie incoming) => throw new NotSupportedException(); protected override Task>> UpdateMetadata( MediaItemScanResult result, MovieMetadata fullMetadata, CancellationToken cancellationToken) => throw new NotSupportedException(); private static async IAsyncEnumerable> EmptyMovies() { await Task.CompletedTask; yield break; } } }