150 lines
5.4 KiB
C#
150 lines
5.4 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using NSubstitute;
|
|
using PLib.Application.Abstractions;
|
|
using PLib.Application.Library;
|
|
using PLib.Domain.Videos;
|
|
using Shouldly;
|
|
|
|
namespace PLib.Tests.Library;
|
|
|
|
public sealed class LibraryServiceTests
|
|
{
|
|
private const string Root = @"C:\videos";
|
|
|
|
private readonly InMemoryVideoRepository _repository = new();
|
|
private readonly IVideoFileScanner _scanner = Substitute.For<IVideoFileScanner>();
|
|
private readonly IMediaProbe _probe = Substitute.For<IMediaProbe>();
|
|
private readonly IThumbnailGenerator _thumbnails = Substitute.For<IThumbnailGenerator>();
|
|
|
|
public LibraryServiceTests()
|
|
{
|
|
_probe.ProbeAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
|
.Returns(new VideoTechnicalInfo(TimeSpan.FromMinutes(2), 1920, 1080, "h264"));
|
|
|
|
_thumbnails.GetOrCreateAsync(Arg.Any<string>(), Arg.Any<TimeSpan?>(), Arg.Any<CancellationToken>())
|
|
.Returns(callInfo => $@"C:\cache\{Path.GetFileNameWithoutExtension(callInfo.Arg<string>())}.jpg");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Files_that_are_new_on_disk_are_added_probed_and_given_a_thumbnail()
|
|
{
|
|
GivenFilesOnDisk(File(@"C:\videos\a.mp4"), File(@"C:\videos\b.mkv"));
|
|
|
|
var events = await CollectAsync(CreateService());
|
|
|
|
_repository.Items.Count.ShouldBe(2);
|
|
_repository.Items.ShouldAllBe(x => x.IsIndexed);
|
|
|
|
events.OfType<LibraryScanEvent.ItemAdded>().Count().ShouldBe(2);
|
|
events.OfType<LibraryScanEvent.ItemUpdated>().Count().ShouldBe(2);
|
|
events.OfType<LibraryScanEvent.Completed>().Single().LibrarySize.ShouldBe(2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Entries_whose_file_is_gone_are_dropped_from_the_library()
|
|
{
|
|
_repository.Seed(new VideoItem(@"C:\videos\stale.mp4", "stale", 5_000, DateTimeOffset.UnixEpoch));
|
|
GivenFilesOnDisk(File(@"C:\videos\a.mp4"));
|
|
|
|
var events = await CollectAsync(CreateService());
|
|
|
|
_repository.Items.Select(x => x.FullPath).ShouldBe([@"C:\videos\a.mp4"]);
|
|
events.OfType<LibraryScanEvent.ItemRemoved>().Count().ShouldBe(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Files_below_the_minimum_size_are_not_part_of_the_library()
|
|
{
|
|
GivenFilesOnDisk(File(@"C:\videos\tiny.mp4", sizeInBytes: 128), File(@"C:\videos\real.mp4"));
|
|
|
|
await CollectAsync(CreateService(new LibraryOptions { MinimumFileSizeInBytes = 1_024 }));
|
|
|
|
_repository.Items.Select(x => x.FullPath).ShouldBe([@"C:\videos\real.mp4"]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task An_item_that_is_already_indexed_is_not_probed_again()
|
|
{
|
|
var indexed = new VideoItem(@"C:\videos\a.mp4", "a", 5_000, DateTimeOffset.UnixEpoch);
|
|
indexed.ApplyTechnicalInfo(new VideoTechnicalInfo(TimeSpan.FromMinutes(1), 1280, 720, "h264"));
|
|
indexed.AttachThumbnail(@"C:\cache\a.jpg");
|
|
_repository.Seed(indexed);
|
|
|
|
GivenFilesOnDisk(File(@"C:\videos\a.mp4", sizeInBytes: 5_000));
|
|
|
|
await CollectAsync(CreateService());
|
|
|
|
await _probe.DidNotReceive().ProbeAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task An_item_whose_file_changed_on_disk_is_indexed_again()
|
|
{
|
|
var indexed = new VideoItem(@"C:\videos\a.mp4", "a", 5_000, DateTimeOffset.UnixEpoch);
|
|
indexed.ApplyTechnicalInfo(new VideoTechnicalInfo(TimeSpan.FromMinutes(1), 1280, 720, "h264"));
|
|
indexed.AttachThumbnail(@"C:\cache\old.jpg");
|
|
_repository.Seed(indexed);
|
|
|
|
GivenFilesOnDisk(File(@"C:\videos\a.mp4", sizeInBytes: 9_999));
|
|
|
|
await CollectAsync(CreateService());
|
|
|
|
await _probe.Received(1).ProbeAsync(@"C:\videos\a.mp4", Arg.Any<CancellationToken>());
|
|
_repository.Items.Single().ThumbnailPath.ShouldBe(@"C:\cache\a.jpg");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task The_same_file_reached_through_two_overlapping_roots_is_only_added_once()
|
|
{
|
|
GivenFilesOnDisk(File(@"C:\videos\a.mp4"));
|
|
|
|
var events = await CollectAsync(CreateService(), Root, Root);
|
|
|
|
_repository.Items.Count.ShouldBe(1);
|
|
events.OfType<LibraryScanEvent.DiscoveryCompleted>().Single().FilesFound.ShouldBe(1);
|
|
}
|
|
|
|
private static DiscoveredVideoFile File(string path, long sizeInBytes = 10_000) =>
|
|
new(path, sizeInBytes, DateTimeOffset.UnixEpoch);
|
|
|
|
private void GivenFilesOnDisk(params DiscoveredVideoFile[] files) =>
|
|
_scanner.ScanAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
|
.Returns(_ => files.ToAsyncEnumerable());
|
|
|
|
private LibraryService CreateService(LibraryOptions? options = null) => new(
|
|
_repository,
|
|
_scanner,
|
|
_probe,
|
|
_thumbnails,
|
|
Options.Create(options ?? new LibraryOptions { MinimumFileSizeInBytes = 0 }),
|
|
NullLogger<LibraryService>.Instance);
|
|
|
|
private static async Task<List<LibraryScanEvent>> CollectAsync(
|
|
LibraryService service,
|
|
params string[] folders)
|
|
{
|
|
var events = new List<LibraryScanEvent>();
|
|
|
|
await foreach (var scanEvent in service.ScanAsync(folders.Length == 0 ? [Root] : folders))
|
|
{
|
|
events.Add(scanEvent);
|
|
}
|
|
|
|
return events;
|
|
}
|
|
}
|
|
|
|
internal static class AsyncEnumerableExtensions
|
|
{
|
|
public static async IAsyncEnumerable<T> ToAsyncEnumerable<T>(this IEnumerable<T> source)
|
|
{
|
|
foreach (var item in source)
|
|
{
|
|
yield return item;
|
|
}
|
|
|
|
await Task.CompletedTask;
|
|
}
|
|
}
|