162 lines
6.4 KiB
C#
162 lines
6.4 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using NSubstitute;
|
|
using PLib.Application.Abstractions;
|
|
using PLib.Application.Library;
|
|
using PLib.Application.Metadata;
|
|
using PLib.Domain.Videos;
|
|
using Shouldly;
|
|
|
|
namespace PLib.Tests.Library;
|
|
|
|
/// <summary>
|
|
/// The library-wide run. Its job is not to find metadata — that is the provider's — but to
|
|
/// decide what to ask about, what to write without asking, and when to stop asking a source.
|
|
/// </summary>
|
|
public sealed class MetadataScanTests
|
|
{
|
|
private readonly InMemoryVideoRepository _videos = new();
|
|
private readonly InMemoryLabelRepository _labels = new();
|
|
private readonly IMetadataProvider _provider = Substitute.For<IMetadataProvider>();
|
|
|
|
private readonly MetadataSourceOptions _source =
|
|
new() { Name = "StashDB", Endpoint = "https://stashdb.org/graphql" };
|
|
|
|
[Fact]
|
|
public async Task Only_fingerprinted_videos_are_asked_about()
|
|
{
|
|
var hashed = Video("с отпечатком", hash: 1);
|
|
_videos.Seed(hashed, Video("без отпечатка", hash: null));
|
|
|
|
Answer(_source, []);
|
|
|
|
var events = await CollectAsync();
|
|
|
|
// Without a fingerprint there is no question to ask, so the video is not counted as
|
|
// processed either — a total that included it would never reach itself.
|
|
events.OfType<MetadataScanEvent.Completed>().Single().Processed.ShouldBe(1);
|
|
await _provider.Received(1).FindByPerceptualHashAsync(_source, 1, Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Videos_that_already_have_a_description_are_skipped_by_default()
|
|
{
|
|
var described = Video("описанное", hash: 1);
|
|
described.Describe("уже есть");
|
|
_videos.Seed(described, Video("голое", hash: 2));
|
|
|
|
Answer(_source, []);
|
|
|
|
(await CollectAsync()).OfType<MetadataScanEvent.Completed>().Single().Processed.ShouldBe(1);
|
|
|
|
// Asking again for everything is what the switch is for.
|
|
var all = await CollectAsync(new MetadataScanRequest(OnlyWithoutDescription: false));
|
|
all.OfType<MetadataScanEvent.Completed>().Single().Processed.ShouldBe(2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_single_candidate_is_written_only_when_the_run_was_told_it_may()
|
|
{
|
|
var video = Video("видео", hash: 1);
|
|
_videos.Seed(video);
|
|
|
|
Answer(_source, [Match("Название")]);
|
|
|
|
await CollectAsync();
|
|
video.Description.ShouldBeNull();
|
|
|
|
var applied = await CollectAsync(new MetadataScanRequest(ApplyUnambiguous: true));
|
|
|
|
video.Title.ShouldBe("Название");
|
|
applied.OfType<MetadataScanEvent.Completed>().Single().Applied.ShouldBe(1);
|
|
applied.OfType<MetadataScanEvent.Matched>().Single().Applied.ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Disagreeing_sources_are_never_written_without_asking()
|
|
{
|
|
var video = Video("видео", hash: 1);
|
|
_videos.Seed(video);
|
|
|
|
var second = new MetadataSourceOptions { Name = "Другой", Endpoint = "https://other/graphql" };
|
|
Answer(_source, [Match("Одно")]);
|
|
Answer(second, [Match("Другое")]);
|
|
|
|
var events = await CollectAsync(new MetadataScanRequest(ApplyUnambiguous: true), _source, second);
|
|
|
|
// Two candidates is precisely the case a human has to resolve.
|
|
video.Title.ShouldBe("видео");
|
|
events.OfType<MetadataScanEvent.Matched>().Single().Applied.ShouldBeFalse();
|
|
events.OfType<MetadataScanEvent.Completed>().Single().Applied.ShouldBe(0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_failing_source_is_reported_once_and_then_left_out_of_the_run()
|
|
{
|
|
_videos.Seed(Video("первое", hash: 1), Video("второе", hash: 2), Video("третье", hash: 3));
|
|
|
|
_provider.FindByPerceptualHashAsync(_source, Arg.Any<ulong>(), Arg.Any<CancellationToken>())
|
|
.Returns<IReadOnlyList<VideoMetadataMatch>>(_ => throw new HttpRequestException("401"));
|
|
|
|
var events = await CollectAsync();
|
|
|
|
// A rejected key fails on every video; three identical lines would bury the results,
|
|
// and three hundred would be the whole page.
|
|
events.OfType<MetadataScanEvent.SourceAbandoned>().ShouldHaveSingleItem();
|
|
await _provider.Received(1).FindByPerceptualHashAsync(
|
|
_source,
|
|
Arg.Any<ulong>(),
|
|
Arg.Any<CancellationToken>());
|
|
|
|
// The run still finishes, having gone through every video and found nothing.
|
|
events.OfType<MetadataScanEvent.Completed>().Single().Processed.ShouldBe(3);
|
|
}
|
|
|
|
private void Answer(MetadataSourceOptions source, IReadOnlyList<VideoMetadataMatch> matches) =>
|
|
_provider.FindByPerceptualHashAsync(source, Arg.Any<ulong>(), Arg.Any<CancellationToken>())
|
|
.Returns(matches);
|
|
|
|
private static VideoMetadataMatch Match(string title) =>
|
|
new("StashDB", "id", title, "описание", [], [], []);
|
|
|
|
private static VideoItem Video(string title, ulong? hash)
|
|
{
|
|
var item = new VideoItem($@"C:\videos\{title}.mp4", title, 1_000, DateTimeOffset.UnixEpoch);
|
|
item.ApplyTechnicalInfo(new VideoTechnicalInfo(TimeSpan.FromMinutes(1), 1280, 720, "h264"));
|
|
item.ApplyPerceptualHash(hash);
|
|
return item;
|
|
}
|
|
|
|
private async Task<List<MetadataScanEvent>> CollectAsync(
|
|
MetadataScanRequest? request = null,
|
|
params MetadataSourceOptions[] sources)
|
|
{
|
|
var service = new LibraryService(
|
|
_videos,
|
|
_labels,
|
|
Substitute.For<IVideoFileScanner>(),
|
|
Substitute.For<IMediaProbe>(),
|
|
Substitute.For<IThumbnailGenerator>(),
|
|
Substitute.For<IAnimatedPreviewGenerator>(),
|
|
Substitute.For<IVideoPerceptualHasher>(),
|
|
_provider,
|
|
Options.Create(new LibraryOptions()),
|
|
|
|
// No pause between requests: the delay exists to be kind to somebody else's
|
|
// server, and there isn't one here.
|
|
MetadataMonitor.With(sources.Length == 0 ? [_source] : sources, delayMilliseconds: 0),
|
|
NullLogger<LibraryService>.Instance);
|
|
|
|
var events = new List<MetadataScanEvent>();
|
|
|
|
await foreach (var scanEvent in service.ScanMetadataAsync(
|
|
request ?? new MetadataScanRequest(),
|
|
TestContext.Current.CancellationToken))
|
|
{
|
|
events.Add(scanEvent);
|
|
}
|
|
|
|
return events;
|
|
}
|
|
}
|