97 lines
3.1 KiB
C#
97 lines
3.1 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 DuplicateDetectionTests
|
|
{
|
|
private readonly InMemoryVideoRepository _videos = new();
|
|
|
|
[Fact]
|
|
public void Distance_counts_the_bits_that_differ()
|
|
{
|
|
var left = Video("a", 0b1111);
|
|
var right = Video("b", 0b1010);
|
|
|
|
left.DistanceTo(right).ShouldBe(2);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_video_without_a_hash_has_no_distance_to_anything()
|
|
{
|
|
var hashed = Video("a", 1);
|
|
var bare = new VideoItem(@"C:\videos\b.mp4", "b", 1, DateTimeOffset.UnixEpoch);
|
|
|
|
hashed.DistanceTo(bare).ShouldBeNull();
|
|
bare.DistanceTo(hashed).ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Near_identical_videos_are_grouped_and_unrelated_ones_are_not()
|
|
{
|
|
_videos.Seed(
|
|
Video("original", 0b0000_0000),
|
|
Video("re-encode", 0b0000_0011),
|
|
Video("unrelated", 0b1111_1111));
|
|
|
|
var groups = await CreateService().FindDuplicatesAsync(maxDistance: 4, Token);
|
|
|
|
groups.ShouldHaveSingleItem();
|
|
groups[0].Select(x => x.Title).OrderBy(x => x).ShouldBe(["original", "re-encode"]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_chain_of_near_matches_ends_up_in_one_group()
|
|
{
|
|
// Ends are 4 bits apart — further than the threshold — but the middle copy links
|
|
// them, and all three are the same film.
|
|
_videos.Seed(
|
|
Video("a", 0b0000_0000),
|
|
Video("b", 0b0000_0011),
|
|
Video("c", 0b0000_1111));
|
|
|
|
var groups = await CreateService().FindDuplicatesAsync(maxDistance: 2, Token);
|
|
|
|
groups.ShouldHaveSingleItem();
|
|
groups[0].Count.ShouldBe(3);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Videos_that_were_never_hashed_are_left_out_entirely()
|
|
{
|
|
_videos.Seed(
|
|
new VideoItem(@"C:\videos\x.mp4", "x", 1, DateTimeOffset.UnixEpoch),
|
|
new VideoItem(@"C:\videos\y.mp4", "y", 1, DateTimeOffset.UnixEpoch));
|
|
|
|
var groups = await CreateService().FindDuplicatesAsync(maxDistance: 64, Token);
|
|
|
|
// Two hashless videos are not evidence of anything, however wide the threshold.
|
|
groups.ShouldBeEmpty();
|
|
}
|
|
|
|
private static CancellationToken Token => TestContext.Current.CancellationToken;
|
|
|
|
private static VideoItem Video(string title, ulong hash)
|
|
{
|
|
var item = new VideoItem($@"C:\videos\{title}.mp4", title, 1, DateTimeOffset.UnixEpoch);
|
|
item.ApplyPerceptualHash(hash);
|
|
return item;
|
|
}
|
|
|
|
private LibraryService CreateService() => new(
|
|
_videos,
|
|
new InMemoryLabelRepository(),
|
|
Substitute.For<IVideoFileScanner>(),
|
|
Substitute.For<IMediaProbe>(),
|
|
Substitute.For<IThumbnailGenerator>(),
|
|
Substitute.For<IAnimatedPreviewGenerator>(),
|
|
Substitute.For<IVideoPerceptualHasher>(),
|
|
Options.Create(new LibraryOptions()),
|
|
NullLogger<LibraryService>.Instance);
|
|
}
|