using System.IO.Abstractions; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Plex; using ErsatzTV.Scanner.Core.Interfaces; using ErsatzTV.Scanner.Core.Interfaces.Metadata; using ErsatzTV.Scanner.Core.Metadata; using ErsatzTV.Scanner.Core.Plex; 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 other-video library // FileNotFound (which EmptyTrash could then permanently delete). Assert the sweep is skipped. public class MediaServerOtherVideoLibraryScannerTests { [TestFixture] public class CleanupFileNotFoundItems { [Test] public async Task Empty_Incoming_With_Existing_OtherVideos_Does_Not_Flag() { var otherVideoRepository = Substitute.For(); var scannerProxy = Substitute.For(); var library = new PlexLibrary { Id = 9, Name = "Other Videos" }; otherVideoRepository.GetExistingOtherVideos(library) .Returns(new List { new() { Key = "ov-1", State = MediaItemState.Normal }, new() { Key = "ov-2", State = MediaItemState.Normal } }); var scanner = new TestOtherVideoLibraryScanner(scannerProxy); Either result = await scanner.Scan(otherVideoRepository, library); result.IsRight.ShouldBeTrue(); await otherVideoRepository.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. The // connection parameters are likewise never dereferenced on the empty path (passed null! below). private sealed class TestOtherVideoLibraryScanner : MediaServerOtherVideoLibraryScanner< PlexConnectionParameters, PlexLibrary, PlexOtherVideo, PlexItemEtag> { public TestOtherVideoLibraryScanner(IScannerProxy scannerProxy) : base( scannerProxy, Substitute.For(), Substitute.For(), Substitute.For(), Substitute.For()) { } public Task> Scan( IPlexOtherVideoRepository otherVideoRepository, PlexLibrary library) => ScanLibrary( otherVideoRepository, null!, library, _ => string.Empty, false, CancellationToken.None); protected override IAsyncEnumerable> GetOtherVideoLibraryItems( PlexConnectionParameters connectionParameters, PlexLibrary library, MediaServerProjectionFailureCounter projectionFailures) => EmptyOtherVideos(); protected override string MediaServerItemId(PlexOtherVideo otherVideo) => otherVideo.Key; protected override string MediaServerEtag(PlexOtherVideo otherVideo) => otherVideo.Etag; protected override Task> GetFullMetadata( PlexConnectionParameters connectionParameters, PlexLibrary library, MediaItemScanResult result, PlexOtherVideo incoming, bool deepScan) => throw new NotSupportedException(); protected override Task>> GetFullMetadataAndStatistics( PlexConnectionParameters connectionParameters, PlexLibrary library, MediaItemScanResult result, PlexOtherVideo incoming) => throw new NotSupportedException(); protected override Task>> UpdateMetadata( MediaItemScanResult result, OtherVideoMetadata fullMetadata, CancellationToken cancellationToken) => throw new NotSupportedException(); private static async IAsyncEnumerable> EmptyOtherVideos() { await Task.CompletedTask; yield break; } } }